├── 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 | 
54 | ## IP段扫描
55 | 
56 | ## 批量扫描
57 | 
58 | ## modbus协议识别(以罗克韦尔和施耐德为例)
59 |
60 |
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 |
--------------------------------------------------------------------------------