├── ModbusPLC_InfoScan.py ├── README.md └── SiemensPLC_InfoScan.py /ModbusPLC_InfoScan.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import sys 3 | from ipaddress import ip_network 4 | import ipaddress 5 | import time 6 | from datetime import datetime 7 | from prettytable import PrettyTable 8 | 9 | GREEN = '\033[92m' 10 | YELLOW = '\033[93m' 11 | RESET = '\033[0m' 12 | ORANGE = '\033[91m' 13 | 14 | 15 | def print_result(addr,printable_response): 16 | table = PrettyTable() 17 | table.field_names = ["addr", "info"] 18 | table.add_row([addr, printable_response]) 19 | print(table) 20 | 21 | def parse_ip_range(ip_range): 22 | if '-' in ip_range: 23 | start, end = ip_range.split('-') 24 | start_ip = ipaddress.IPv4Address(start.strip()) 25 | try: 26 | end_ip = ipaddress.IPv4Address(end.strip()) 27 | return range(int(start_ip), int(end_ip) + 1) 28 | except ipaddress.AddressValueError: 29 | return [start] 30 | elif '/' in ip_range: 31 | return [str(ip) for ip in ipaddress.IPv4Network(ip_range, strict=False).hosts()] 32 | else: 33 | return [ip_range] 34 | 35 | def save_result_to_file(result, filename): 36 | with open(filename, 'a') as f: 37 | f.write(result) 38 | 39 | def print_copyright(): 40 | print(f''' 41 | __________________________________________ 42 | {GREEN} ____ ___ ___ ___ __ _ _ 43 | (_ _)/ __)/ __) / __) /__\ ( \( ) 44 | _)(_( (__ \__ \( (__ /(__)\ ) ( 45 | (____)\___)(___/ \___)(__)(__)(_)\_){RESET} 46 | 47 | Identify for Modbus protocol 48 | ver1.0 by 01dGu0 & Novy 49 | __________________________________________ 50 | ''') 51 | 52 | def print_progress_bar(): 53 | sys.stdout.write(f'\r{YELLOW}[SCHEDULE]{RESET} ' + YELLOW + "/" + RESET) 54 | sys.stdout.flush() 55 | time.sleep(0.2) 56 | sys.stdout.write(f'\r{YELLOW}[SCHEDULE]{RESET} ' + YELLOW + "-" + RESET) 57 | sys.stdout.flush() 58 | time.sleep(0.2) 59 | sys.stdout.write(f'\r{YELLOW}[SCHEDULE]{RESET} ' + YELLOW + "\\" + RESET) 60 | sys.stdout.flush() 61 | time.sleep(0.2) 62 | sys.stdout.write(f'\r{YELLOW}[SCHEDULE]{RESET} ' + YELLOW + "|" + RESET) 63 | sys.stdout.flush() 64 | time.sleep(0.2) 65 | 66 | def send_modbus_request(ip, port=502, request_data="00 00 00 00 00 05 00 2b 0e 01 00", timeout=5): 67 | print(f'{YELLOW}[CUTRENT INFO] {RESET} {ip}') 68 | 69 | client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 70 | client.settimeout(timeout) 71 | 72 | try: 73 | progress_bar_length = 8 74 | for _ in range(progress_bar_length): 75 | print_progress_bar() 76 | # print("\n") 77 | 78 | client.connect((ip, port)) 79 | 80 | request_bytes = bytes.fromhex(request_data) 81 | client.sendall(request_bytes) 82 | 83 | response = client.recv(1024) 84 | 85 | if not response or len(response) < 5: 86 | print(f"\n{YELLOW}[PROMPT] {RESET}{ip}:{port}") 87 | addr = f"{ip}:{port}" 88 | print_result(addr,"当前为工控设备但无法识别详细信息") 89 | else: 90 | ascii_response = response.decode('ascii', errors='ignore') 91 | printable_response = ''.join(filter(lambda x: x.isprintable(), ascii_response)) 92 | 93 | # 如果转换后的信息为空或长度低于五个字符就该这么办 94 | if not printable_response or len(printable_response) < 5: 95 | print(f"\n{YELLOW}[PROMPT] {RESET}{ip}:{port}") 96 | addr = f"{ip}:{port}" 97 | print_result(addr,"当前为工控设备但无法识别详细信息") 98 | else: 99 | print(f"\n{GREEN}[+] {RESET}{ip}:{port}") 100 | addr = f"{ip}:{port}" 101 | print_result(addr,printable_response) 102 | 103 | except socket.timeout: 104 | print(f"\n{ORANGE}[ERROR] {RESET}{ip}:{port} - 响应超时,当前目标可能不是Modbus协议或者目标不是工控设备") 105 | pass 106 | except Exception as e: 107 | print(f"\n{ORANGE}[ERROR] {RESET}{ip}:{port} : {e}") 108 | pass 109 | finally: 110 | client.close() 111 | 112 | def scan_subnet(subnet, port=502): 113 | for ip in ip_network(subnet): 114 | send_modbus_request(str(ip), port) 115 | 116 | if __name__ == "__main__": 117 | print_copyright() 118 | if len(sys.argv) < 2: 119 | print(f""" 120 | {ORANGE}[ERROR] {RESET}Please enter the IP or IP segment, e.g. 121 | python script.py 0.0.0.0 --default 502 122 | python script.py 0.0.0.0:502 123 | python script.py 0.0.0.0/24 124 | """) 125 | sys.exit(1) 126 | timestamp = datetime.now().strftime("%Y%m%d%H%M%S") 127 | filename = f"Schneider_results_{timestamp}.txt" 128 | target = sys.argv[1] 129 | if '-' in target: 130 | for ipa in parse_ip_range(target): 131 | scan_subnet(str(ipa)) 132 | # if result_c is not None and result_1 is not None: 133 | # save_result_to_file(result_c + result_1, filename) 134 | elif '/' in target: 135 | for ipb in ipaddress.IPv4Network(target, strict=False): 136 | scan_subnet(str(ipb)) 137 | # if result_c is not None and result_1 is not None: 138 | # save_result_to_file(result_c + result_1, filename) 139 | else: 140 | if ':' in target: 141 | ipc, port_str = target.split(':') 142 | porta = int(port_str) 143 | else: 144 | ipc = target 145 | porta = 502 146 | send_modbus_request(ipc, porta) 147 | # if result_c is not None and result_1 is not None: 148 | # save_result_to_file(result_c + result_1, filename) 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ICS Tools 2 | 工控设备信息识别工具箱,旨在快速识别攻防演练中轨道交通、燃气、水利等行业内网场景、传统工业内网中的工业控制设备型号,工具相比ISF和NMAP等工具更简单易用且体积更小。 3 | ## 注: 4 | - 当前已支持Modbus协议设备和西门子设备的识别,使用过程中如遇bug或其他错误请联系作者或进行反馈,如果你有好的建议也可告诉我们 5 | - 目前modbus协议识别支持常见的施耐德、罗克韦尔等品牌,modbus默认端口为502,你可根据实际情况指定端口;部分modbus协议设备信息可能返回为空,在设备识别的数据处理上存在部分问题,例如返回的设备信息中存在无关字符,后续将优化 6 | - 后续将更新包括但不局限于cip、umas、fins的协议 7 | # 协议识别 8 | ## 西门子S7协议识别: 9 | ```bash 10 | $ python3 SiemensPLC_InfoScan.py 11 | 12 | __________________________________________ 13 | ____ ___ ___ ___ __ _ _ 14 | (_ _)/ __)/ __) / __) /__\ ( \( ) 15 | _)(_( (__ \__ \( (__ /(__)\ ) ( 16 | (____)\___)(___/ \___)(__)(__)(_)\_) 17 | 18 | Identify for Siemens equipment 19 | ver1.0 by 01dGu0 & Novy 20 | __________________________________________ 21 | 22 | usage: SiemensPLC_InfoScan.py [-h] [-f FILE] [-p IP] 23 | 24 | optional arguments: 25 | -h, --help show this help message and exit 26 | -f FILE, --file FILE Specify txt file, batch scanning, one IP per line 27 | -p IP, --ip IP Specify IP or IP range (e.g., 192.168.1.1 or 192.168.1.0/24) 28 | 29 | [ERROR] Please provide at least one parameter (-f or -p). 30 | ``` 31 | ## modbus协议识别 32 | ```bash 33 | $ python3 ModbusPLC_InfoScan.py 34 | 35 | __________________________________________ 36 | ____ ___ ___ ___ __ _ _ 37 | (_ _)/ __)/ __) / __) /__\ ( \( ) 38 | _)(_( (__ \__ \( (__ /(__)\ ) ( 39 | (____)\___)(___/ \___)(__)(__)(_)\_) 40 | 41 | Identify for Modbus protocol 42 | ver1.0 by 01dGu0 & Novy 43 | __________________________________________ 44 | 45 | 46 | [ERROR] Please enter the IP or IP segment, e.g. 47 | python script.py 0.0.0.0 --default 502 48 | python script.py 0.0.0.0:502 49 | python script.py 0.0.0.0/24 50 | ``` 51 | # 扫描 52 | ## 单IP扫描 53 | ![image](https://github.com/Fupo-series/ICS-Tools/assets/45167857/81aea0c2-4ff9-4b07-9623-41067071edc2) 54 | ## IP段扫描 55 | ![image](https://github.com/Fupo-series/ICS-Tools/assets/45167857/b135bfcd-0494-4931-8d98-c2b5206fe520) 56 | ## 批量扫描 57 | ![image](https://github.com/Fupo-series/ICS-Tools/assets/45167857/5fe98798-a605-42db-8fb6-560638ea4fa5) 58 | ## modbus协议识别(以罗克韦尔和施耐德为例) 59 | image 60 | image 61 | 62 | 63 | -------------------------------------------------------------------------------- /SiemensPLC_InfoScan.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import argparse 3 | from binascii import unhexlify, hexlify 4 | from datetime import datetime 5 | import ipaddress 6 | from prettytable import PrettyTable 7 | import sys 8 | import time 9 | 10 | GREEN = '\033[92m' 11 | YELLOW = '\033[93m' 12 | RESET = '\033[0m' 13 | ORANGE = '\033[91m' 14 | 15 | def getSZL001c(Respons): 16 | result = '' 17 | for i in range(int(len(Respons) / 68)): 18 | data = Respons[i * 68 + 4:i * 68 + 68].replace("00", "") 19 | try: 20 | if unhexlify(data).decode("utf-8", "ignore") != "": 21 | result += unhexlify(data).decode("utf-8", "ignore") + '\n' 22 | except: 23 | pass 24 | return result or '' 25 | 26 | def getSZL0011(Respons): 27 | result = '' 28 | for i in range(int(len(Respons) / 56)): 29 | data = Respons[i * 56 + 4:i * 56 + 56].replace("00", "") 30 | try: 31 | if unhexlify(data).decode("utf-8", "ignore") != "": 32 | result += unhexlify(data).decode("utf-8", "ignore") + '\n' 33 | except: 34 | pass 35 | return result or '' 36 | 37 | def print_progress_bar(): 38 | sys.stdout.write(f'\r{YELLOW}[SCHEDULE]{RESET} ' + YELLOW + "/" + RESET) 39 | sys.stdout.flush() 40 | time.sleep(0.2) 41 | sys.stdout.write(f'\r{YELLOW}[SCHEDULE]{RESET} ' + YELLOW + "-" + RESET) 42 | sys.stdout.flush() 43 | time.sleep(0.2) 44 | sys.stdout.write(f'\r{YELLOW}[SCHEDULE]{RESET} ' + YELLOW + "\\" + RESET) 45 | sys.stdout.flush() 46 | time.sleep(0.2) 47 | sys.stdout.write(f'\r{YELLOW}[SCHEDULE]{RESET} ' + YELLOW + "|" + RESET) 48 | sys.stdout.flush() 49 | time.sleep(0.2) 50 | 51 | 52 | def save_result_to_file(result, filename): 53 | with open(filename, 'a') as f: 54 | f.write(result) 55 | 56 | def print_copyright(): 57 | print(f''' 58 | __________________________________________ 59 | {GREEN} ____ ___ ___ ___ __ _ _ 60 | (_ _)/ __)/ __) / __) /__\ ( \( ) 61 | _)(_( (__ \__ \( (__ /(__)\ ) ( 62 | (____)\___)(___/ \___)(__)(__)(_)\_){RESET} 63 | 64 | Identify for Siemens equipment 65 | ver1.0 by 01dGu0 & Novy 66 | __________________________________________ 67 | ''') 68 | 69 | def print_result(ip_address, result_c, result_1): 70 | table = PrettyTable() 71 | table.field_names = ["ip", "product", "other"] 72 | table.add_row([ip_address, result_c, result_1]) 73 | print(table) 74 | 75 | def parse_ip_range(ip_range): 76 | if '-' in ip_range: 77 | start, end = ip_range.split('-') 78 | start_ip = ipaddress.IPv4Address(start.strip()) 79 | try: 80 | end_ip = ipaddress.IPv4Address(end.strip()) 81 | return range(int(start_ip), int(end_ip) + 1) 82 | except ipaddress.AddressValueError: 83 | return [start] 84 | elif '/' in ip_range: 85 | return [str(ip) for ip in ipaddress.IPv4Network(ip_range, strict=False).hosts()] 86 | else: 87 | return [ip_range] 88 | 89 | def main(ip_address): 90 | try: 91 | print(f'{GREEN}[CUTRENT INFO] {RESET} {ip_address}') 92 | 93 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 94 | sock.connect((ip_address, 102)) 95 | 96 | progress_bar_length = 20 97 | for _ in range(progress_bar_length): 98 | print_progress_bar() 99 | 100 | print(f'\r{GREEN}[SCHEDULE]{RESET} {YELLOW}Done.{RESET}\n') 101 | 102 | sock.send(unhexlify("0300001611e00000000100c0010ac1020100c2020102")) 103 | sock.recv(1024) 104 | sock.send(unhexlify("0300001902f08032010000080000080000f0000001000101e0")) 105 | sock.recv(1024) 106 | sock.send(unhexlify("0300002102f080320700000a00000800080001120411440100ff090004001c0000")) 107 | Respons = (hexlify(sock.recv(1024)).decode())[82:] 108 | result_c = getSZL001c(Respons) 109 | 110 | sock.send(unhexlify("0300002102f080320700000a00000800080001120411440100ff09000400110000")) 111 | Respons = (hexlify(sock.recv(1024)).decode())[82:] 112 | result_1 = getSZL0011(Respons) 113 | 114 | print_result(ip_address, result_c, result_1) 115 | 116 | sock.close() 117 | return result_c, result_1 118 | except Exception as e: 119 | print(f'{ORANGE}[ERROR]{RESET} {e}') 120 | pass 121 | return None, None 122 | 123 | if __name__ == '__main__': 124 | parser = argparse.ArgumentParser() 125 | parser.add_argument('-f', '--file', help='Specify txt file, batch scanning, one IP per line') 126 | parser.add_argument('-p', '--ip', help='Specify IP or IP range (e.g., 192.168.1.1 or 192.168.1.0/24)') 127 | 128 | args = parser.parse_args() 129 | 130 | if not (args.file or args.ip): 131 | print_copyright() 132 | parser.print_help() 133 | print(f'\n{ORANGE}[ERROR]{RESET} Please provide at least one parameter (-f or -p).') 134 | else: 135 | print_copyright() 136 | timestamp = datetime.now().strftime("%Y%m%d%H%M%S") 137 | filename = f"Siemens_results_{timestamp}.txt" 138 | 139 | if args.file: 140 | print(f'{GREEN}[TARGET] {RESET}', args.file) 141 | with open(args.file, 'r') as file: 142 | for line in file: 143 | ip = line.strip() 144 | if '-' in ip: 145 | for ip in parse_ip_range(ip): 146 | result_c, result_1 = main(str(ip)) 147 | if result_c is not None and result_1 is not None: 148 | save_result_to_file(result_c + result_1, filename) 149 | elif '/' in ip: 150 | for ip in ipaddress.IPv4Network(ip, strict=False): 151 | result_c, result_1 = main(str(ip)) 152 | if result_c is not None and result_1 is not None: 153 | save_result_to_file(result_c + result_1, filename) 154 | else: 155 | result_c, result_1 = main(ip) 156 | if result_c is not None and result_1 is not None: 157 | save_result_to_file(result_c + result_1, filename) 158 | elif args.ip: 159 | print(f'{GREEN}[TARGET] {RESET}', args.ip) 160 | if '-' in args.ip: 161 | for ip in parse_ip_range(args.ip): 162 | result_c, result_1 = main(str(ip)) 163 | if result_c is not None and result_1 is not None: 164 | save_result_to_file(result_c + result_1, filename) 165 | elif '/' in args.ip: 166 | for ip in ipaddress.IPv4Network(args.ip, strict=False): 167 | result_c, result_1 = main(str(ip)) 168 | if result_c is not None and result_1 is not None: 169 | save_result_to_file(result_c + result_1, filename) 170 | else: 171 | result_c, result_1 = main(args.ip) 172 | if result_c is not None and result_1 is not None: 173 | save_result_to_file(result_c + result_1, filename) 174 | 175 | print(f'{GREEN}[FILE]{RESET}:{filename}') 176 | --------------------------------------------------------------------------------