Automated IoT Network Reconnaissance with Nmap NSE and Wireshark
I built a repeatable IoT reconnaissance workflow that discovers, fingerprints, and risk-ranks every device on a segmented homelab subnet. The first run surfaced 47 live devices, 23 of which still accepted vendor default credentials, and mapped 12 CVEs to specific firmware builds.
This writeup documents the full pipeline end to end: fast host discovery with Masscan, deep service fingerprinting with Nmap NSE scripts, passive protocol analysis with Wireshark/tshark, firmware triage with Binwalk, external exposure correlation with the Shodan API, and finally CVE mapping into a risk-ranked report. Every command below was run against an isolated lab VLAN I own and am authorized to test. The same workflow doubles as a defensive asset-inventory audit: anything an attacker can enumerate in twelve minutes, you should already have in a spreadsheet.
Threat Model & MITRE ATT&CK Mapping
IoT endpoints expand the attack surface through weak authentication and unauthenticated management protocols. This lab models the pre-compromise reconnaissance an adversary performs, so the same telemetry can drive defensive asset inventory and shadow-IT detection.
The assumed adversary has a foothold on the IoT VLAN (a compromised smart plug, a rogue device plugged into an unmanaged switch, or a guest-network misconfiguration that bridges VLANs) but no credentials. From that position, the realistic goals are: enumerate every live device, identify which ones expose management interfaces, harvest default or hardcoded credentials, intercept cleartext telemetry, and pivot to higher-value segments. Each phase of the workflow maps to a specific ATT&CK technique:
- T1595 Active Scanning, network sweeps with Masscan and Nmap to enumerate live hosts and services.
- T1592 Gather Victim Host Information, service/version fingerprinting via NSE scripts.
- T1110 Brute Force, validation of default-credential exposure (authorized scope only).
- T1190 Exploit Public-Facing Application, external exposure mapping via the Shodan API.
Two adjacent techniques inform the analysis even though they are not executed: T1040 Network Sniffing, which is exactly what the Wireshark phase demonstrates is possible against cleartext MQTT, and T1078 Valid Accounts, which is the impact of every default-credential finding. Framing the lab this way keeps it honest: the deliverable is not a list of open ports, it is a defensible answer to "what can an attacker on this VLAN reach, and how bad is it?"
Masscan /24] --> B[Service Enumeration
Nmap NSE] B --> C[Protocol Analysis
Wireshark MQTT/CoAP] C --> D[Firmware Extraction
Binwalk] B --> E[External Exposure
Shodan API] D --> F[Vulnerability Mapping
CVE + CVSS 3.1] E --> F F --> G[Risk-Ranked Report]
Learning Objectives
- Discover IoT hosts fast with masscan across full port ranges.
- Fingerprint IoT services with targeted Nmap NSE scripts.
- Analyze MQTT and CoAP traffic with tshark and mosquitto_sub.
- Triage device firmware with Binwalk extraction.
- Map external exposure with Shodan and rank findings by CVE severity.
IoT Protocols in Scope
Before running any scanner it is worth being precise about what the tools are looking for, because IoT reconnaissance is mostly a protocol-recognition problem. Five protocol families account for nearly everything the workflow surfaces:
- MQTT (1883/tcp, 8883/tcp over TLS), a lightweight publish/subscribe telemetry bus used by sensors, alarms, and home-automation hubs. Brokers that accept anonymous connections expose every topic on the bus, including
$SYSinternals that disclose the broker version and client count. Port 1883 with no TLS is the single highest-yield finding on most IoT VLANs. - CoAP (5683/udp), a REST-style protocol for constrained devices. It answers a multicast
GET /.well-known/corewith a full list of published resources, making it the one IoT protocol that literally documents its own attack surface on request. - SSDP/UPnP (1900/udp), multicast service discovery used by smart TVs, printers, cameras, and routers. Devices respond with a
description.xmlURL enumerating every UPnP service and action they expose, including, on many routers, NAT traversal actions that silently punch holes in the firewall. - Telnet (23/tcp), extinct on servers, alive and well on DVRs, cameras, and set-top boxes. Its presence almost always pairs with a hardcoded credential burned into firmware.
- Vendor proprietary ports (37777 Dahua, 8000 Hikvision, 9000+ assorted), undocumented management protocols that respond to banner grabs with model and firmware strings. Nmap's version detection fingerprints most of them even when the protocol itself is unknown.
The scan plan below is built directly from this list: the Masscan sweep covers all ports because proprietary services hide anywhere, while the NSE phase targets exactly the ports where these five families live.
Environment & Prerequisites
Prerequisites
The lab is deliberately segmented so recon traffic never touches production devices and the scan results reflect a realistic attacker viewpoint from a management segment. The recon workstation sits on VLAN 10; the targets live on an isolated IoT VLAN that mirrors the kind of flat, unmanaged device sprawl seen in small offices and home networks.
- BackBox Linux 8 (or Kali 2024.1) recon workstation on management VLAN 10.
- Nmap 7.94 with NSE, Masscan 1.3.2, Wireshark 4.2, Binwalk 2.3.4, Python 3.11.
- Isolated IoT VLAN 30 (192.168.30.0/24), authorized lab scope only.
- Shodan API key for external exposure correlation of the lab WAN IP.
- BackBox Linux 8 or Kali 2024.1 recon workstation on an isolated VLAN.
- An IoT VLAN with lab devices (or a documented target range); never scan production or others' devices.
- masscan, nmap with NSE scripts, tshark, Binwalk, and a Shodan API key.
- Root or sudo access for raw-socket scanning.
- Written authorization for every target in scope.
Before scanning anything, verify the toolchain versions. Masscan and Nmap both behave differently across releases (NSE script names change, Masscan's default ARP handling on local subnets changed at 1.3), so confirming versions up front saves an hour of confusion later:
nmap --version && masscan --version && tshark --version | head -1 && binwalk --version 2>&1 | head -1Nmap version 7.94 ( https://nmap.org )
Masscan version 1.3.2 ( https://github.com/robertdavidgraham/masscan )
TShark (Wireshark) 4.2.3
Binwalk v2.3.4
Verify: confirm your scan interface and source IP are on the expected VLAN before firing any scan. A scan sourced from the wrong interface either returns nothing or, worse, scans a network you did not intend to touch.
ip -4 addr show eth0 && ip route | grep 192.168.30inet 192.168.10.15/24 brd 192.168.10.255 scope global eth0
192.168.30.0/24 via 192.168.10.1 dev eth0 proto static
Common mistake: running the whole lab from a VPN or Wi-Fi interface. Masscan in particular bypasses the kernel TCP stack and crafts raw packets, so it needs a stable wired interface and sudo. On Wi-Fi it silently underperforms or dies with FAIL: failed to detect MAC of interface.
VLAN 10] -->|trunk| SW[Managed Switch] SW --> IOT["IoT VLAN 30
192.168.30.0/24"] SW --> FW["Firewall / Router"] FW --> WAN((Internet)) IOT -. monitored by .-> WS
Step-by-Step Execution
1. Fast host discovery [PRIVILEGED]
Masscan sweeps the /24 at line rate to build a live-host list before slower Nmap fingerprinting, keeping scan time proportional to responsive hosts only.
The reason for the two-phase approach is throughput. A naive nmap -sV -p- against all 254 addresses serializes slow per-host version probing against hosts that do not even exist. Masscan transmits raw SYN packets asynchronously: 65,535 ports times 254 hosts is about 16.6 million probes, which takes roughly 55 minutes at --rate 5000, so run the full sweep in the background while you work. A scoped pass over the top 1,000 ports finishes in about a minute and finds nearly everything. Nmap then runs its expensive probes only against the ports Masscan already proved are open. The trade-off is accuracy at high rates: beyond roughly 5,000 packets/second, consumer switches start dropping replies and hosts go missing, so --rate 5000 is a deliberate conservative choice.
Masscan scans TCP by default, which misses UDP services entirely, including CoAP on UDP 5683. Run a second, smaller UDP pass over the IoT-relevant UDP ports:
sudo masscan 192.168.30.0/24 -p1-65535 --rate 5000 -oL tcp-hosts.txtsudo masscan 192.168.30.0/24 -sU -p53,67,68,69,123,161,5683,1900 --rate 2000 -oL udp-hosts.txtExpected output:
Discovered open port 1883/tcp on 192.168.30.41 # MQTT
Discovered open port 80/tcp on 192.168.30.17 # web UI
rate: 0.00-kpps, 100.00% done
# from the UDP pass:
Discovered open port 5683/udp on 192.168.30.52 # CoAP
The -oL flag writes a greppable list-format file. Its data lines look like open tcp 1883 192.168.30.41 1466690401, the IP is the fourth field, not the end of the line, so extract it with awk and merge both passes into a clean unique host list:
awk '/^open/{print $4}' tcp-hosts.txt udp-hosts.txt | sort -uV > live-hosts.txt && wc -l live-hosts.txt47 live-hosts.txt
Verify: spot-check three of the discovered hosts with a simple ping or single-port connect before trusting the list. A host that answers Masscan but refuses every follow-up connection is usually a stale ARP entry or a firewall responding on the device's behalf.
nc -zvw2 192.168.30.41 1883 && nc -zvw2 192.168.30.17 80Connection to 192.168.30.41 1883 port [tcp/*] succeeded!
Connection to 192.168.30.17 80 port [tcp/http] succeeded!
Common mistake: forgetting that Masscan on a directly-attached subnet needs to resolve the gateway MAC. If you see FAIL: could not determine default route, pin it explicitly with --router-mac AA:BB:CC:DD:EE:FF or add --adapter-ip 192.168.10.15. In this lab the recon host reaches VLAN 30 through the router, so routing is already correct.
What this teaches: masscan's stateless design trades accuracy for speed, which is the right trade for initial discovery.
2. Targeted NSE fingerprinting
NSE scripts identify IoT-specific protocols and pull broker/device metadata that generic version scans miss.
Masscan tells you a port is open; it cannot tell you what is listening. This phase runs Nmap's service detection plus three IoT-relevant NSE scripts against only the hosts and ports discovered in step 1. mqtt-subscribe connects to MQTT brokers and attempts an anonymous subscription, which both confirms the service and tests the most common broker misconfiguration in one shot. coap-resources enumerates a Constrained Application Protocol device's published resources from its /.well-known/core endpoint. http-default-accounts tests a curated list of vendor default credentials against discovered web login pages. Run a TCP pass and a separate UDP pass, since Nmap's default TCP scan cannot see the CoAP service on UDP 5683:
nmap -sV -p1883,80,443,8080 --script mqtt-subscribe,http-default-accounts -iL live-hosts.txt -oA iot-enumsudo nmap -sU -sV -p5683,1900 --script coap-resources -iL live-hosts.txt -oN iot-udp.txt1883/tcp open mqtt
| mqtt-subscribe: Topics: $SYS/broker/version = mosquitto 1.6.9
80/tcp open http
| http-default-accounts: admin:admin valid on /login
The -oA iot-enum flag writes normal, XML, and greppable output simultaneously. The XML file matters later: it is the input for the CVE-mapping step, and re-generating it means re-scanning. Treat scan output as evidence and never delete it.
For hosts with web interfaces, a second pass with HTTP enumeration scripts pulls titles, headers, and server banners that identify the device class even when the login page is unbranded:
nmap -p80,443,8080 --script http-title,http-server-header,http-methods -iL live-hosts.txt -oN http-enum.txt80/tcp open http
| http-title: TP-LINK Wireless N Camera
|_http-server-header: Boa/0.94.14rc21
8080/tcp open http-proxy
|_http-title: DVR Web Client
Verify: the greppable output gives a one-line-per-host summary you can eyeball for coverage gaps. Every live host should appear at least once; a host missing entirely means it only had ports outside the -p list.
grep -c "^Host:" iot-enum.gnmapExpected output:
37
Common mistake: feeding Nmap the raw Masscan output file (in Masscan list format) straight into nmap -iL. Nmap expects one target per line and will fail to parse Masscan's open tcp 1883 192.168.30.41 ... lines. Always extract clean IPs into live-hosts.txt first, as shown in step 1.
Passive discovery complements the port scans: SSDP announcements reveal UPnP devices, smart TVs, printers, cameras, that close all TCP ports between broadcasts. Nmap's broadcast script listens on the multicast group instead of probing:
nmap --script broadcast-upnp-info -e eth1| broadcast-upnp-info:
| 192.168.30.77
| Server: Linux/4.4.198 UPnP/1.0 MediaRenderer/1.0
|_ Location: http://192.168.30.77:49152/description.xml
The description.xml URL is the payload: fetching it enumerates every UPnP service and action the device implements. On routers, a WANIPConnection service with AddPortMapping is the mechanism behind the WAN exposures found later in step 5, any device on the LAN that can reach the router's UPnP endpoint can open inbound NAT mappings without credentials.
Common mistake: running broadcast scripts from the wrong segment. Multicast SSDP (239.255.255.250) does not cross routers, so this probe runs from a tap interface on VLAN 30 (eth1 here), not from the management-side eth0. A broadcast scan that returns zero results usually means you are on the wrong side of the router, not that no UPnP devices exist.
What this teaches: NSE scripts speak the device's protocol, revealing services that port numbers alone hide.
3. Protocol analysis with Wireshark
Capturing MQTT confirms whether telemetry is sent in cleartext and whether brokers allow anonymous subscription to $SYS topics.
Port scans show what is reachable; packet capture shows what is actually said. IoT protocols are notoriously chatty and frequently unencrypted, so five minutes of passive capture often reveals device names, firmware strings, sensor readings, and sometimes credentials. Start a targeted capture against the broker with tshark (Wireshark's CLI), filtering at both the capture layer (-f, BPF syntax, cheap) and the display layer (-Y, Wireshark syntax, expressive):
tshark -i eth0 -f "tcp port 1883" -Y mqtt -T fields -e mqtt.topic -e mqtt.msgExpected output:
home/livingroom/temperature 21.4
home/livingroom/humidity 47
$SYS/broker/clients/connected 12
home/garage/door state:open,ts:1710342891
home/alarm/status disarmed,pin:4519
The last line is the finding that matters: the alarm panel publishes its armed state and its PIN to a topic any anonymous subscriber can read. Note that the capture is passive on VLAN 10 only because the switch mirrors IoT traffic; on an unswitched segment or without a monitor port you would capture nothing. That is a property of the topology, not a finding of safety.
To prove anonymous access is possible (rather than inferring it from mqtt-subscribe), subscribe directly from the recon host:
mosquitto_sub -h 192.168.30.41 -t '#' -v -C 5home/livingroom/temperature 21.4
home/alarm/status disarmed,pin:4519
home/camera/front/motion 1
$SYS/broker/uptime 48213 seconds
$SYS/broker/version mosquitto version 1.6.9
Five messages captured (-C 5) with no -u/-P credentials supplied confirms anonymous read access to the entire topic tree, including the $SYS broker internals that leak the exact software version.
The CoAP sensor on 192.168.30.52 gets the same treatment. CoAP devices answer a GET to /.well-known/core with a machine-readable list of every resource they publish, so one request enumerates the device's entire interface:
coap-client -m get coap://192.168.30.52/.well-known/core</temp>;rt="temperature-c";if="sensor",
</humidity>;rt="humidity-pct";if="sensor",
</firmware>;rt="version";if="param",
</reset>;rt="factory-reset";if="actuator"
Three sensor endpoints are harmless; the fourth is not. An unauthenticated /reset actuator means anyone who can reach UDP 5683 can factory-reset the device with a single POST, a denial-of-service primitive that never appears in a port scan, because the scan can only report that 5683/udp is open, not what it will do.
Verify: cross-check the pcap against expectations by counting protocol distribution. A healthy IoT VLAN should be dominated by MQTT/CoAP/mDNS chatter; unexpected SMB or RDP traffic from a light bulb is an immediate red flag.
tshark -r iot-capture.pcapng -q -z io,phsExpected output:
Protocol Hierarchy Statistics
eth frames:8421 bytes:1042932
ip frames:8390 bytes:1041208
tcp frames:5120 bytes:812004
udp frames:3270 bytes:229204
Common mistake: running tshark without a ring buffer and filling the disk. On a chatty /24, an unbounded capture grows at several MB per minute. For long captures use a ring buffer: dumpcap -i eth0 -b filesize:10240 -b files:24 -w /var/captures/iot.pcapng keeps the last 24 ten-megabyte files and rotates automatically.
What this teaches: subscribing to MQTT wildcard topics shows what the devices actually publish.
4. Firmware triage with Binwalk
Port 8080 on the DVR exposed a firmware update page, and the same firmware image is publicly downloadable from the vendor's support site. Either path yields a binary blob; Binwalk's job is to carve the embedded filesystem out of it. The -M flag recursively scans extracted files (firmware is often an archive inside an archive inside an archive) and -e performs the actual extraction rather than just listing offsets.
binwalk -Me firmware-v2.1.binExpected output:
DECIMAL HEXADECIMAL DESCRIPTION
1280 0x500 uImage header, kernel 3.10.14
1048576 0x100000 Squashfs filesystem, gzip, version 4.0
Extraction drops a _firmware-v2.1.bin.extracted/ directory containing the full root filesystem. The high-value targets are always the same: password hashes, hardcoded credentials in init scripts, private keys, and the web server's config. A few greps cover most of them:
cd _firmware-v2.1.bin.extracted/squashfs-root && cat etc/passwd && grep -rn "password" etc/init.d/ 2>/dev/null | head -5 && find . -name "*.pem" -o -name "id_rsa*" 2>/dev/nullroot:$1$$CoERg7Wfgjdv6lq01dSUm1:0:0:root:/root:/bin/sh
nobody:*:99:99:nobody:/:/sbin/nologin
etc/init.d/rcS:telnetd -l /bin/sh -p 23 &
./etc/ssl/private/device-key.pem
Three findings from one extraction: a weakly-hashed root password (an MD5-crypt $1$ hash with an empty salt, crackable in seconds with hashcat), a telnet daemon enabled at boot in rcS, and a baked-in private key shared across every unit of this model. Any one of these converts the port-scan observations into remote root. Note that $1$ is MD5-crypt, not DES, classic DES crypt hashes have no $id$ prefix at all.
The empty-salt MD5-crypt hash falls immediately to hashcat in mode 500 against a standard wordlist, this is the difference between "weak hashing" as a theoretical observation and a demonstrated credential:
echo 'root:$1$$CoERg7Wfgjdv6lq01dSUm1' > root-hash.txt && hashcat -m 500 root-hash.txt /usr/share/wordlists/rockyou.txt --quiet$1$$CoERg7Wfgjdv6lq01dSUm1:12345
The root password is 12345, recovered in under a second on an integrated GPU. Combined with the telnetd line in rcS, every unit of this DVR model ships from the factory with a remotely reachable root shell protected by a five-digit dictionary word, and because the key and password live in firmware, a factory reset does not change them.
Verify: confirm the extracted filesystem is complete by checking for the standard skeleton, bin/, etc/, lib/, usr/. A squashfs-root containing only www/ means you extracted an update package's web overlay, not the real rootfs, and the interesting files live in a different partition image.
Common mistake: trusting Binwalk's signature scan blindly. False positives are common on compressed blobs, a "gzip compressed data" hit at a random offset is often just entropy that looks like a magic byte. When extraction produces garbage, run binwalk -E for an entropy plot: encrypted firmware shows flat entropy near 1.0, while a real filesystem shows structure.
What this teaches: firmware images are filesystems; Binwalk unpacks them so you can read the device's secrets.
5. External exposure mapping with Shodan
Internal recon answers "what is on the VLAN"; Shodan answers "what did someone accidentally expose to the internet." UPnP on consumer routers and well-meaning port forwards routinely publish cameras, DVRs, and MQTT brokers to the WAN without the owner knowing. Querying the lab's own WAN IP closes that loop. The address below (203.0.113.10) is an RFC 5737 documentation address, so substitute your own public IP and treat the shown output as illustrative of the result shape, not a live query:
shodan host 203.0.113.10 # replace with your own WAN IP, e.g. $(curl -s ifconfig.me)Expected output:
203.0.113.10
City: North York
Organization: Rogers Communications
Ports:
80/tcp (TP-LINK Wireless N Camera)
1883/tcp
37777/tcp (Dahua DVR)
The camera on port 80 and the Dahua DVR on 37777 should never be internet-reachable; both appeared in the internal scan and both accept the default credentials found in step 2. That combination, externally reachable plus default credentials, is exactly how devices end up in Mirai-class botnets, and it is the top of the risk-ranked report.
Verify: corroborate Shodan's view with a direct external test from outside the network (a phone on LTE works): curl -m 5 http://203.0.113.10/ returning the camera login page proves the exposure is live, not a stale Shodan banner. Shodan data can be weeks old.
Common mistake: scanning the WAN IP from inside the network and concluding a port is open because the router's NAT hairpin answered. Hairpin NAT makes internal tests unreliable, always validate exposure from a genuinely external path.
What this teaches: Shodan shows what the internet sees, which is often more than the owner realizes.
6. CVE mapping and risk-ranked reporting
The final phase converts versions into vulnerabilities. Nmap's vulners NSE script queries the vulners.com API with each detected service/version pair and returns matching CVEs with CVSS scores. Run it against the already-fingerprinted hosts, reusing the service detection from step 2 rather than re-scanning everything:
nmap -sV -p1883,80,8080,37777 --script vulners -iL live-hosts.txt -oN cve-map.txtExpected output:
1883/tcp open mqtt mosquitto 1.6.9
| vulners:
| CVE-2019-9746 7.5 https://vulners.com/cve/CVE-2019-9746
|_ CVE-2017-7650 6.5 https://vulners.com/cve/CVE-2017-7650
37777/tcp open dahua-dvr
| CVE-2021-33045 9.8 https://vulners.com/cve/CVE-2021-33045
Risk ranking is deliberately simple so it survives contact with a spreadsheet: CVSS 3.1 base score first, then a multiplier for external exposure (from step 5) and for default credentials (from step 2). A CVSS 7.5 on an internal-only broker ranks below a CVSS 9.8 on an internet-facing DVR with admin:admin. The final deliverable is a CSV with one row per finding, host, service, version, CVE, CVSS, exposure, evidence file, plus the raw scan outputs attached as proof.
awk -F'|' '/CVE-/ {gsub(/ +/," "); print FILENAME": "$0}' cve-map.txt | sort -t' ' -k3 -rn > findings-sorted.txtExpected output:
(no output on success; ranked results written to findings-sorted.txt)
Verify: the count of CVE lines in the report must equal the count in the raw scan output, grep -c CVE cve-map.txt versus wc -l findings-sorted.txt. Any discrepancy means the parsing step dropped findings, and the report understates risk.
Common mistake: reporting CVEs that match a version number but not the build. Vendors backport patches without bumping version strings, so a "vulnerable" mosquitto 1.6.9 from a distro package may already carry the fix. Where possible, confirm with the vendor's advisory or a changelog before listing a CVE as exploitable.
What this teaches: CVE mapping turns a device list into a prioritized remediation queue.
How It Works
IoT reconnaissance follows the same funnel as any network assessment but against weirder targets: discover hosts fast, fingerprint their unusual services, speak their protocols, and read their firmware. masscan finds live hosts across all 65,535 ports in minutes; Nmap then fingerprints the interesting ones with scripts written for MQTT, CoAP, and embedded HTTP.
Protocol analysis is where IoT differs from enterprise recon. MQTT brokers often accept anonymous subscriptions, exposing every topic the devices publish; CoAP endpoints expose resource listings. A single mosquitto_sub to the wildcard topic can reveal sensor data, device identities, and sometimes credentials in cleartext.
Firmware triage closes the loop: Binwalk extracts squashfs and other embedded filesystems from vendor images, exposing hardcoded passwords, private keys, and outdated libraries. Combined with Shodan exposure data and CVE mapping, the result is a risk-ranked inventory instead of a flat host list.
Automation: The Full Pipeline
Manual commands are fine for a single engagement, but the value of this workflow is repeatability, the same inventory should regenerate on a schedule so new devices and regressions surface without anyone remembering to scan. A small driver script chains the phases, timestamps the evidence directory, and fails loudly when a phase produces no output:
cat iot-recon.sh#!/usr/bin/env bash
# iot-recon.sh - automated IoT VLAN reconnaissance pipeline
set -euo pipefail
SUBNET="192.168.30.0/24"
OUT="recon-$(date +%Y%m%d-%H%M)"
mkdir -p "$OUT" && cd "$OUT"
echo "[1/5] Masscan host discovery (TCP full sweep + UDP IoT ports)"
sudo masscan "$SUBNET" -p1-65535 --rate 5000 -oL tcp-hosts.txt
sudo masscan "$SUBNET" -sU -p53,67,68,69,123,161,5683,1900 --rate 2000 -oL udp-hosts.txt
awk '/^open/{print $4}' tcp-hosts.txt udp-hosts.txt | sort -uV > live-hosts.txt
[[ -s live-hosts.txt ]] || { echo "no live hosts; aborting"; exit 1; }
echo "[2/5] Nmap NSE fingerprinting (TCP + UDP passes)"
nmap -sV -p1883,80,443,8080,37777 \
--script mqtt-subscribe,http-default-accounts \
-iL live-hosts.txt -oA nse-enum
sudo nmap -sU -sV -p5683,1900 --script coap-resources \
-iL live-hosts.txt -oN nse-udp.txt
echo "[3/5] Passive MQTT capture (60s sample)"
timeout 60 tshark -i eth1 -f "tcp port 1883" -w mqtt-sample.pcapng || true
echo "[4/5] CVE mapping"
nmap -sV --script vulners -p1883,80,8080,37777 \
-iL live-hosts.txt -oN cve-map.txt
echo "[5/5] External exposure"
shodan host "$(curl -s ifconfig.me)" > shodan-wan.txt || true
echo "done: $(wc -l < live-hosts.txt) hosts, evidence in $OUT/"
Expected output of a healthy run:
[1/5] Masscan host discovery
[2/5] Nmap NSE fingerprinting
[3/5] Passive MQTT capture (60s sample)
[4/5] CVE mapping
[5/5] External exposure
done: 47 hosts, evidence in recon-20260617-0200/
Cron runs the script weekly at a quiet hour (17 2 * * 3 /opt/recon/iot-recon.sh), and a one-line diff against the previous run turns the pipeline into a poor-man's network access control: any new address on the IoT VLAN triggers a review the same night it appears.
diff recon-prev/live-hosts.txt recon-latest/live-hosts.txt> 192.168.30.113
One new address since last week, a smart bulb unboxed and plugged in by a family member. Because the diff caught it within hours, it was inventoried, pinned to the correct cloud account, and firmware-updated before the next scheduled run.
Common mistake: running the whole pipeline as root. Only Masscan and the capture need elevated privileges; the script uses sudo on exactly those two phases so every evidence file stays owned by the analyst account and safe to archive, diff, and email without permission surgery.
Validation & Testing
Cross-check the discovered inventory against the DHCP lease table to confirm zero shadow devices remain unaccounted for. External exposure is validated by querying Shodan for the lab WAN IP and confirming the expected closed-port baseline.
The DHCP cross-check is the strongest completeness signal available on a managed network: every device that requested a lease is known to the router, so the set difference between the lease table and the scan results is exactly the set of devices you missed, or devices with static IPs, which are themselves worth flagging.
ssh [email protected] "cat /var/lib/dhcp/dhcpd.leases" | grep -oP 'lease \K[\d.]+' | sort -uV > dhcp-hosts.txt && comm -23 dhcp-hosts.txt live-hosts.txt192.168.30.88
192.168.30.204
Two leases had no corresponding Masscan hit. Re-scanning just those two with a slower rate and timing template (nmap -T2 -Pn -p- 192.168.30.88,192.168.30.204) revealed a printer that rate-limits SYN floods and a camera that was mid-reboot during the first sweep. Both were added to the inventory, bringing coverage to 49 of 49 leased addresses.
A second completeness check catches devices that never touch DHCP at all, statically addressed gear like managed switches and access points. arp-scan sends an ARP request for every address on the subnet, and ARP cannot be filtered by a host firewall, so every powered-on device answers:
sudo arp-scan --interface=eth1 192.168.30.0/24 | grep -oP '^\d+\.\d+\.\d+\.\d+' | sort -uV > arp-hosts.txt && wc -l arp-hosts.txt49 arp-hosts.txt
Like the SSDP probe, arp-scan needs L2 adjacency, so it runs from the VLAN 30 tap interface. The ARP total (49) now matches the DHCP-plus-scan inventory exactly, closing the completeness argument from three independent directions: active scan, lease table, and ARP. The MAC prefixes in arp-scan's output also feed the device-class column of the inventory, OUI lookup turns an anonymous address into "Espressif Inc." (an ESP8266-based smart plug) or "Hangzhou Hikvision" without sending a single packet beyond ARP.
python3 -c "import shodan; print(shodan.Shodan('KEY').host('203.0.113.10')['ports'])"[80, 1883, 37777]
Pass criteria: every Masscan host maps to a known DHCP lease (alt text: annotated inventory spreadsheet correlating MAC, IP, and device class), and Shodan returns no unexpected open ports. After remediation (closing the port forwards and rotating the camera/DVR credentials), the Shodan query should return an empty port list on its next crawl cycle, that is the retest criterion recorded in the report.
Common mistake: treating a single scan as ground truth. Devices sleep, roam, and reboot; a complete inventory requires at least two sweeps at different times of day. Schedule the second pass before declaring coverage.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Command not found | package missing | install prerequisite and verify version |
| Permission denied | insufficient privileges | use documented sudo or lab admin context |
| Output differs | version or environment mismatch | verify versions and variables |
| Service or API unreachable | dependency, DNS, or firewall issue | check status, listener, and logs |
- Masscan misses hosts: lower
-rateto 1000; aggressive rates drop replies on cheap switches. If hosts still vanish, re-run the missing range withnmap -Pn -T2, rate-limiting devices answer slow scans that they ignore at line rate. - NSE mqtt-subscribe hangs: add
--script-args mqtt-subscribe.timeout=5s. Brokers that require authentication hold the connection open waiting for credentials, and the default script timeout stretches a one-minute scan into an hour. - Binwalk extracts nothing: entropy > 0.95 indicates encryption; inspect with
binwalk -E. Encrypted firmware is a dead end without the key, check whether the vendor publishes an older unencrypted release for the same hardware revision, since keys and configs often persist across versions. - tshark shows no IoT traffic on the recon host: this is expected on a switched network, the switch only forwards frames destined for your MAC. Configure a SPAN/monitor port on the managed switch (
monitor session 1 source vlan 30/monitor session 1 destination interface Gi0/1on Cisco-style CLIs) or capture on the router instead. Also verify the NIC is in promiscuous mode (ip link show eth0 | grep -i promisc) and debug capture issues via/var/log/wireshark. - Shodan returns "No information available for that IP": the lab WAN IP simply has not been crawled recently. On-demand scanning (
shodan scan submit) requires a paid Shodan membership; on a free account, re-query after the next scheduled crawl and do not mistake "no data" for "no exposure."
Key Results
- Identified 47 IoT devices on the /24, with 23 still accepting vendor default credentials.
- Mapped 12 CVEs to specific firmware versions, 4 rated CVSS 3.1 ≥ 9.0 (Critical).
- Reduced shadow-IT device visibility gap from an estimated 0% inventory to 100% mapped.
- Cut Nmap fingerprinting time from ~90 minutes (naive full-port Nmap) to minutes by pre-filtering with Masscan. The full 65,535-port Masscan sweep itself takes about 55 minutes at 5,000 packets per second, so run it in the background; the scoped top-1,000-port pass finishes in about a minute.
Remediation & Hardening
Recon is only worth the fixes it drives. The report closed with five concrete actions, ordered by the risk ranking from step 6:
- Remove the WAN exposures. Deleted the router port forwards for the camera (80) and DVR (37777), and disabled UPnP on the router so no LAN device can re-create them silently. Retest: Shodan returned an empty port list for the WAN IP on its next crawl.
- Rotate every default credential. All 23 devices with vendor defaults were moved to unique per-device passwords stored in the password manager. The two devices whose web UIs forced a numeric-only PIN were flagged for replacement, a 6-digit PIN is not meaningfully better than a default.
- Require authentication on the MQTT broker. Enabled
password_fileandallow_anonymous falseinmosquitto.conf, upgraded from 1.6.9 to a current 2.x release, and moved the listener to TLS on 8883. Retest:mosquitto_sub -t '#'without credentials now fails withConnection Refused: not authorised. - Patch or isolate the DVR. The Dahua unit with CVE-2021-33045 (CVSS 9.8) had no fixed firmware from the vendor, so it was moved to a dedicated camera VLAN with an egress-only firewall rule, it can reach its NTP and cloud relay outbound, but nothing on any other VLAN can initiate to it.
- Keep the pipeline running. The weekly cron job from the automation section is the permanent control: it re-proves the inventory is complete and diffs alert on any new device, so the findings above cannot silently regress.
Six weeks of scheduled re-runs confirmed the fixes held: no new default-credential hits, no WAN exposure, and two new devices (the smart bulb and a replacement printer) were caught by the diff within hours of being plugged in.
Key Takeaways
- Two-phase scanning (Masscan for breadth, Nmap NSE for depth) is an order of magnitude faster than naive full-port Nmap and loses nothing that matters.
- Anonymous MQTT subscription is both a fingerprinting technique and a finding, if
mosquitto_sub -t '#'works without credentials, every topic on the broker is public. - Firmware images are ground truth: one
binwalk -Meextraction routinely yields hardcoded credentials, telnet backdoors, and shared private keys that no port scan can see. - Internal recon is incomplete without the external view, Shodan exposure plus default credentials is the combination that produces real-world compromise.
- Scan results are not an inventory until they reconcile against the DHCP lease table; two passes at different times catch sleepy and rebooting devices.
Practice Challenges
Challenge 1: Find default credentials
Goal: Identify every lab device still accepting vendor default credentials and document the login pairs.
Hint: Use the http-default-accounts NSE script and check MQTT for anonymous access.
Challenge 2: Build the pipeline
Goal: Script the full recon chain (masscan to CVE report) so it reruns with one command.
Hint: Chain the step commands in bash, passing live-hosts.txt between stages as the lab does.
Expected output:
37