OT/ICS Shodan Searches

PurposeShodan query
Modbus/TCPport:502
Siemens S7port:102
EtherNet/IPport:44818
EtherNet/IP + UDPport:44818,2222
BACnet/IPport:47808
DNP3port:20000
IEC 60870-5-104port:2404
OPC UAport:4840
MQTTport:1883
Secure MQTTport:8883
Fox / Ignitionport:1911
GE-SRTPport:18245
Niagara Foxport:1911 product:"Niagara"
Siemens S7 fingerprintport:102 product:"Siemens"
Modbus fingerprintport:502 product:"Modbus"
Rockwell/Allen-Bradleyport:44818 product:"EtherNet/IP"
Schneider/Modiconport:502 product:"Modbus"
Australian exposureport:502 country:AU
Australian Siemensport:102 country:AU
Australian EtherNet/IPport:44818 country:AU
Australian DNP3port:20000 country:AU
Australian IEC-104port:2404 country:AU
port:502 country:AU "Schneider Electric"

port:102 "Siemens" 
port:44818 "Allen-Bradley"
port:502 "Modicon"
port:1911 "Niagara"
port:4840 "OPC UA"


port:47808
port:20000
port:2404
port:4840
port:1911
CVEShort descriptionExploitability
CVE-2017-6030Predictable values in the PLC’s security mechanism can make certain security functions predictable. Affected M221 firmware is < 1.5.0.0. (CISA)High — remotely exploitable, low skill; CISA rated it CVSS 7.5.
CVE-2017-7574Hard-coded AES key protects SoMachine Basic project files. With an affected project file, the protection password can be recovered and the project opened/modified. Specifically affects TM221CE16R 1.3.3.3. (NVD)High impact, but requires the protected project file. CVSS 9.8.
CVE-2017-7575A specially crafted Modbus request can disclose the application-protection password, allowing the PLC application to subsequently be downloaded/modified/uploaded. (NVD)Critical — remote, unauthenticated, low complexity. CVSS 9.8.
CVE-2018-7798Insufficient verification of network-configuration data can allow an attacker to change the PLC’s IP address, subnet mask and gateway. Affects all M221 versions. (NVD)High — remote, unauthenticated, low complexity. CVSS 8.2.
CVE-2019-6820Missing authentication allows modification of the PLC’s IP configuration using a specially crafted Ethernet frame. Affects all M221 versions. (NVD)High — remote/network-level attack, no authentication required.
CVE-2020-28214Uses a predictable password-hash salt, allowing precomputation/dictionary techniques to weaken password protection. Affects all M221 versions. (NVD)Medium in NVD’s base assessment; primarily useful when an attacker can obtain the relevant password hash/material.
#!/usr/bin/env python3

import argparse
import socket
import sys
from dataclasses import dataclass


@dataclass
class Vulnerability:
    cve: str
    description: str
    affected_before: str | None = None
    affected_all_versions: bool = False


VULNERABILITIES = [
    Vulnerability(
        "CVE-2017-6030",
        "Predictable TCP initial sequence numbers",
        affected_before="1.5.0.0",
    ),
    Vulnerability(
        "CVE-2017-7574",
        "Hard-coded cryptographic key in project protection",
        affected_before="1.5.0.0",
    ),
    Vulnerability(
        "CVE-2017-7575",
        "Weak application-protection authentication",
        affected_before="1.5.0.2",
    ),
    Vulnerability(
        "CVE-2018-7789",
        "Improper handling of unusual/exceptional conditions",
        affected_before="1.6.2.0",
    ),
    Vulnerability(
        "CVE-2018-7790",
        "Authentication sequence replay",
        affected_before="1.6.2.0",
    ),
    Vulnerability(
        "CVE-2018-7791",
        "Improper access control",
        affected_before="1.6.2.0",
    ),
    Vulnerability(
        "CVE-2018-7792",
        "Missing authorization / weak cryptography",
        affected_before="1.6.2.0",
    ),
    Vulnerability(
        "CVE-2018-7798",
        "Insufficient verification of network configuration data",
        affected_all_versions=True,
    ),
    Vulnerability(
        "CVE-2018-7821",
        "Resource exhaustion involving Ethernet/IP",
        affected_before="1.10.0.0",
    ),
    Vulnerability(
        "CVE-2018-7822",
        "Incorrect default permissions",
        affected_before="1.10.0.0",
    ),
    Vulnerability(
        "CVE-2018-7823",
        "M221/SoMachine Basic security vulnerability",
        affected_before="1.10.0.0",
    ),
    Vulnerability(
        "CVE-2019-6820",
        "Unauthenticated modification of IP configuration",
        affected_all_versions=True,
    ),
    Vulnerability(
        "CVE-2020-28214",
        "Predictable salt in password hashing",
        affected_all_versions=True,
    ),
]


def parse_version(version):
    try:
        return tuple(int(part) for part in version.split("."))
    except ValueError as exc:
        raise ValueError(
            f"Invalid firmware version: {version}"
        ) from exc


def version_is_less_than(current, target):
    return parse_version(current) < parse_version(target)


def check_tcp_port(host, port, timeout):
    try:
        with socket.create_connection(
            (host, port),
            timeout=timeout,
        ):
            return True
    except (socket.timeout, ConnectionRefusedError, OSError):
        return False


def assess_vulnerabilities(firmware):
    results = []

    for vulnerability in VULNERABILITIES:
        if vulnerability.affected_all_versions:
            results.append(
                (
                    vulnerability.cve,
                    "POTENTIALLY AFFECTED",
                    vulnerability.description,
                )
            )
            continue

        if version_is_less_than(
            firmware,
            vulnerability.affected_before,
        ):
            results.append(
                (
                    vulnerability.cve,
                    "POTENTIALLY AFFECTED",
                    vulnerability.description,
                )
            )
        else:
            results.append(
                (
                    vulnerability.cve,
                    "NOT AFFECTED BY VERSION RANGE",
                    vulnerability.description,
                )
            )

    return results


def print_results(host, firmware, port_open, results):
    print()
    print("Modicon M221 Safe Vulnerability Assessment")
    print("=" * 48)
    print(f"Target:   {host}")
    print(f"Firmware: {firmware}")
    print(f"TCP/502:  {'OPEN' if port_open else 'CLOSED/UNREACHABLE'}")
    print()

    print("CVE Assessment")
    print("-" * 48)

    for cve, status, description in results:
        print(f"{cve:<18} {status}")
        print(f"  {description}")

    print()
    print("Safety limitations")
    print("-" * 48)
    print("No authentication bypass was attempted.")
    print("No passwords were requested or recovered.")
    print("No PLC program was downloaded.")
    print("No PLC configuration was modified.")
    print("No registers or coils were written.")
    print("No denial-of-service or packet-flooding test was performed.")
    print()
    print(
        "CVE results are firmware/version assessments and should be "
        "treated as potentially affected until verified against the "
        "vendor advisory and device configuration."
    )


def main():
    parser = argparse.ArgumentParser(
        description=(
            "Safe Modicon M221 vulnerability assessment. "
            "Performs TCP/502 connectivity testing and "
            "firmware-based CVE assessment."
        )
    )

    parser.add_argument(
        "target",
        help="M221 IP address or hostname",
    )

    parser.add_argument(
        "--firmware",
        required=True,
        help="Confirmed M221 firmware version, for example 1.3.3.3",
    )

    parser.add_argument(
        "--timeout",
        type=float,
        default=3.0,
        help="TCP connection timeout in seconds",
    )

    args = parser.parse_args()

    try:
        parse_version(args.firmware)
    except ValueError as exc:
        print(f"Error: {exc}", file=sys.stderr)
        sys.exit(2)

    print(f"Checking {args.target}:502...")

    port_open = check_tcp_port(
        args.target,
        502,
        args.timeout,
    )

    results = assess_vulnerabilities(args.firmware)

    print_results(
        args.target,
        args.firmware,
        port_open,
        results,
    )


if __name__ == "__main__":
    main()
python3 m221_scanner.py 192.168.1.100 --firmware 1.3.3.3
import socket
import sys


def check_modbus(ip, port=502, timeout=3):
    request = bytes.fromhex(
        "000100000006010300000001"
    )

    try:
        with socket.create_connection((ip, port), timeout=timeout) as sock:
            sock.sendall(request)
            response = sock.recv(256)

        if len(response) >= 9 and response[7] == 0x03:
            print(f"[+] {ip}:{port} responded to Modbus/TCP")
            print(f"[+] Response: {response.hex()}")
            return True

        print(f"[-] {ip}:{port} responded, but not as expected")
        print(f"[-] Response: {response.hex()}")

    except (socket.timeout, ConnectionRefusedError, OSError) as exc:
        print(f"[-] Unable to connect to {ip}:{port}: {exc}")

    return False


if __name__ == "__main__":
    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} <PLC-IP>")
        sys.exit(1)

    check_modbus(sys.argv[1])