diff --git a/apps/common/utils/ip/utils.py b/apps/common/utils/ip/utils.py index 14851ff27..4931bccbb 100644 --- a/apps/common/utils/ip/utils.py +++ b/apps/common/utils/ip/utils.py @@ -113,7 +113,6 @@ def get_ip_city(ip): def lookup_domain(domain): try: - return socket.gethostbyname(domain) + return socket.gethostbyname(domain), '' except Exception as e: - print("Cannot resolve %s: Unknown host, %s" % (domain, e)) - return None + return None, f'Cannot resolve {domain}: Unknown host, {e}' diff --git a/apps/settings/tools/__init__.py b/apps/settings/tools/__init__.py new file mode 100644 index 000000000..09154d4b2 --- /dev/null +++ b/apps/settings/tools/__init__.py @@ -0,0 +1,6 @@ +# coding: utf-8 +# +from .ping import * +from .telnet import * +from .nmap import * +from .tcpdump import * diff --git a/apps/settings/tools/nmap.py b/apps/settings/tools/nmap.py new file mode 100644 index 000000000..d58c46824 --- /dev/null +++ b/apps/settings/tools/nmap.py @@ -0,0 +1,57 @@ +import asyncio +import time +import nmap + +from common.utils.timezone import local_now_display +from settings.utils import generate_ips + + +def get_nmap_result(nm, ip, ports, timeout): + results = [] + nm.scan(ip, ports=ports, timeout=timeout) + tcp_port = nm[ip].get('tcp', {}) + udp_port = nm[ip].get('udp', {}) + results.append(f'PORT\tSTATE\tSERVICE') + for port, info in tcp_port.items(): + results.append(f"{port}\t{info.get('state', 'unknown')}\t{info.get('name', 'unknown')}") + for port, info in udp_port.items(): + results.append(f"{port}\t{info.get('state', 'unknown')}\t{info.get('name', 'unknown')}") + return results + + +async def once_nmap(nm, ip, ports, timeout, display): + await display(f'Starting Nmap at {local_now_display()} for {ip}') + try: + is_ok = True + loop = asyncio.get_running_loop() + results = await loop.run_in_executor(None, get_nmap_result, nm, ip, ports, timeout) + for result in results: + await display(result) + + except KeyError: + is_ok = False + await display(f'Host seems down.') + except Exception as err: + is_ok = False + await display(f"Error: %s" % err) + return is_ok + + +async def verbose_nmap(dest_ips, dest_ports=None, timeout=None, display=None): + if not display: + return + + ips = generate_ips(dest_ips) + dest_port = ','.join(list(dest_ports)) if dest_ports else None + + nm = nmap.PortScanner() + success_num, start_time = 0, time.time() + nmap_version = '.'.join(map(lambda x: str(x), nm.nmap_version())) + await display(f'[Summary] Nmap (v{nmap_version}): {len(ips)} addresses were scanned') + for ip in ips: + ok = await once_nmap(nm, str(ip), dest_port, timeout, display) + if ok: + success_num += 1 + await display() + await display(f'[Done] Nmap: {len(ips)} IP addresses ({success_num} hosts up) ' + f'scanned in {round(time.time() - start_time, 2)} seconds') diff --git a/apps/settings/utils/ping.py b/apps/settings/tools/ping.py similarity index 80% rename from apps/settings/utils/ping.py rename to apps/settings/tools/ping.py index cb9e5e544..556bb9e5b 100644 --- a/apps/settings/utils/ping.py +++ b/apps/settings/tools/ping.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # - +import asyncio import os import select import socket @@ -23,16 +23,16 @@ def checksum(source_string): for count in range(0, count_to, 2): this = source_string[count + 1] * 256 + source_string[count] sum = sum + this - sum = sum & 0xffffffff # Necessary? + sum &= 0xffffffff # Necessary? if count_to < len(source_string): - sum = sum + ord(source_string[len(source_string) - 1]) - sum = sum & 0xffffffff # Necessary? + sum += ord(source_string[len(source_string) - 1]) + sum &= 0xffffffff # Necessary? sum = (sum >> 16) + (sum & 0xffff) - sum = sum + (sum >> 16) + sum += sum >> 16 answer = ~sum - answer = answer & 0xffff + answer &= 0xffff # Swap bytes. Bugger me if I know why. answer = answer >> 8 | (answer << 8 & 0xff00) @@ -61,7 +61,7 @@ def receive_one_ping(my_socket, id, timeout): time_sent = struct.unpack("d", received_packet[28: 28 + bytes])[0] return time_received - time_sent - time_left = time_left - how_long_in_select + time_left -= how_long_in_select if time_left <= 0: return @@ -118,7 +118,7 @@ def ping(dest_addr, timeout, psize, flag=0): raise # raise the original error process_pre = os.getpid() & 0xFF00 - flag = flag & 0x00FF + flag &= 0x00FF my_id = process_pre | flag send_one_ping(my_socket, dest_addr, my_id, psize) @@ -128,38 +128,42 @@ def ping(dest_addr, timeout, psize, flag=0): return delay -def verbose_ping(dest_ip, timeout=2, count=5, psize=64, display=None): +async def verbose_ping(dest_ip, timeout=2, count=5, psize=64, display=None): """ Send `count' ping with `psize' size to `dest_addr' with the given `timeout' and display the result. """ - ip = lookup_domain(dest_ip) - if not ip: + if not display: return - if display is None: - display = print + + ip, err = lookup_domain(dest_ip) + if not ip: + await display(err) + return + + await display("PING %s (%s): 56 data bytes" % (dest_ip, ip)) + await asyncio.sleep(0.1) error_count = 0 - display("PING %s (%s): 56 data bytes" % (dest_ip, ip)) for i in range(count): try: delay = ping(dest_ip, timeout, psize) except socket.gaierror as e: - display("failed. (socket error: '%s')" % str(e)) + await display("Failed (socket error: '%s')" % str(e)) error_count += 1 break if delay is None: - display("Request timeout for icmp_seq %i" % i) + await display("Request timeout for icmp_seq %i" % i) error_count += 1 else: delay *= 1000 - display("64 bytes from %s: icmp_seq=0 ttl=115 time=%.3f ms" % (ip, delay)) - time.sleep(1) - display(f'--- {dest_ip} ping statistics ---') - display(f'{count} packets transmitted, ' - f'{count - error_count} packets received, ' - f'{(error_count / count) * 100}% packet loss') - print() + await display("64 bytes from %s: icmp_seq=0 ttl=115 time=%.3f ms" % (ip, delay)) + await asyncio.sleep(1) + + await display(f'--- {dest_ip} ping statistics ---') + await display(f'{count} packets transmitted, ' + f'{count - error_count} packets received, ' + f'{(error_count / count) * 100}% packet loss') if __name__ == "__main__": diff --git a/apps/settings/tools/tcpdump.py b/apps/settings/tools/tcpdump.py new file mode 100644 index 000000000..2b5412c15 --- /dev/null +++ b/apps/settings/tools/tcpdump.py @@ -0,0 +1,98 @@ +import asyncio +import netifaces +import socket +import struct + +from common.utils.timezone import local_now_display +from settings.utils import generate_ips, generate_ports + + +async def once_tcpdump( + interface, src_ips, src_ports, dest_ips, dest_ports, display, stop_event +): + loop = asyncio.get_event_loop() + s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.ntohs(0x0003)) + s.bind((interface, 0)) + s.setblocking(False) + while not stop_event.is_set(): + try: + packet = await loop.sock_recv(s, 65535) + except BlockingIOError: + await asyncio.sleep(0.1) + # 解析IP数据包 + ip_header = packet[14:34] + ip_hdr = struct.unpack('!BBHHHBBH4s4s', ip_header) + # 判断是否为TCP数据包 + protocol = ip_hdr[6] + if protocol != 6: + continue + # 解析TCP数据包 + tcp_header = packet[34:54] + tcp_hdr = struct.unpack('!HHLLBBHHH', tcp_header) + # 获取源地址、源端口号、目标地址、目标端口等信息 + src_ip, dest_ip = map(lambda x: socket.inet_ntoa(x), ip_hdr[8:10]) + src_port, dest_port = tcp_hdr[0], tcp_hdr[1] + # 获取数据包类型和长度 + packet_type = socket.htons(ip_hdr[6]) + packet_len = len(packet) + # 获取TCP标志位、序号、确认号、部分数据等信息 + seq, ack, flags = tcp_hdr[2], tcp_hdr[3], tcp_hdr[5] + data = packet[54:] + # 如果过滤的参数[源地址、源端口等]为空,则不过滤 + # 各个过滤参数之间为 `且` 的关系 + green_light = True + if src_ips and src_ip not in src_ips: + green_light = False + if src_ports and src_port not in src_ports: + green_light = False + if dest_ips and dest_ip not in dest_ips: + green_light = False + if dest_ports and dest_port not in dest_ports: + green_light = False + if not green_light: + continue + + results = [ + f'[{interface}][{local_now_display()}] {src_ip}:{src_port} -> ' + f'{dest_ip}:{dest_port} ({packet_type}, {packet_len} bytes)', + f'\tFlags: {flags} Seq: {seq}, Ack: {ack}', f'\tData: {data}' + ] + for r in results: + await display(r) + + +def list_show(items, default='all'): + return ','.join(map(str, items)) or default + + +async def verbose_tcpdump(interfaces, src_ips, src_ports, dest_ips, dest_ports, display=None): + if not display: + return + + stop_event = asyncio.Event() + valid_interface = netifaces.interfaces() + if interfaces: + valid_interface = set(netifaces.interfaces()) & set(interfaces) + + src_ips = generate_ips(src_ips) + src_ports = generate_ports(src_ports) + dest_ips = generate_ips(dest_ips) + dest_ports = generate_ports(dest_ports) + + summary = [ + f"[Summary] Tcpdump filter info: ", + f"Interface: [{list_show(valid_interface)}]", + f"Source address: [{list_show(src_ips)}]", + f"source port: [{list_show(src_ports)}]", + f"Destination address: [{list_show(dest_ips)}]", + f"Destination port: [{list_show(dest_ports)}]", + ] + for s in summary: + await display(s) + + params = [src_ips, src_ports, dest_ips, dest_ports, display, stop_event] + tasks = [ + asyncio.create_task(once_tcpdump(i, *params)) for i in valid_interface + ] + await asyncio.gather(*tasks) + stop_event.set() diff --git a/apps/settings/tools/telnet.py b/apps/settings/tools/telnet.py new file mode 100644 index 000000000..b54a393d8 --- /dev/null +++ b/apps/settings/tools/telnet.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# +import asyncio + +from common.utils import lookup_domain + +PROMPT_REGEX = r'[\<|\[](.*)[\>|\]]' + + +async def telnet(dest_addr, port_number=23, timeout=10): + try: + reader, writer = await asyncio.wait_for( + asyncio.open_connection(dest_addr, port_number), timeout + ) + except asyncio.TimeoutError: + return False, 'Timeout' + except (ConnectionRefusedError, OSError) as e: + return False, str(e) + try: + # 发送命令 + writer.write(b"command\r\n") + await writer.drain() + # 读取响应 + response = await reader.readuntil() + except asyncio.TimeoutError: + writer.close() + await writer.wait_closed() + return False, 'Timeout' + writer.close() + await writer.wait_closed() + return True, response.decode('utf-8', 'ignore') + + +async def verbose_telnet(dest_ip, dest_port=23, timeout=10, display=None): + if not display: + return + + ip, err = lookup_domain(dest_ip) + if not ip: + await display(err) + return + + await display(f'Trying {dest_ip} ({ip}:{dest_port})') + try: + is_connective, resp = await telnet(dest_ip, dest_port, timeout) + if is_connective: + msg = f'Connected to {dest_ip} {dest_port} {resp}.\r\n' \ + f'Connection closed by foreign host.' + else: + msg = f'Unable to connect to remote host\r\n' \ + f'Reason: {resp}' + except Exception as e: + msg = 'Error: %s' % e + await display(msg) + + +if __name__ == "__main__": + print(verbose_telnet(dest_addr='1.1.1.1', port_number=2222)) + print(verbose_telnet(dest_addr='baidu.com', port_number=80)) + print(verbose_telnet(dest_addr='baidu.com', port_number=8080)) + print(verbose_telnet(dest_addr='192.168.4.1', port_number=2222)) + print(verbose_telnet(dest_addr='192.168.4.1', port_number=2223)) + print(verbose_telnet(dest_addr='ssssss', port_number=-1)) diff --git a/apps/settings/utils/__init__.py b/apps/settings/utils/__init__.py index 8983df3ad..91d05860e 100644 --- a/apps/settings/utils/__init__.py +++ b/apps/settings/utils/__init__.py @@ -3,6 +3,3 @@ from .ldap import * from .common import * -from .ping import * -from .telnet import * -from .nmap import * diff --git a/apps/settings/utils/common.py b/apps/settings/utils/common.py index 3d73e8cdd..e549f9b8c 100644 --- a/apps/settings/utils/common.py +++ b/apps/settings/utils/common.py @@ -1,6 +1,7 @@ # coding: utf-8 from jumpserver.context_processor import default_interface from django.conf import settings +from IPy import IP def get_interface_setting_or_default(): @@ -12,3 +13,56 @@ def get_interface_setting_or_default(): def get_login_title(): return get_interface_setting_or_default()['login_title'] + + +def generate_ips(ip_string): + # 支持的格式 + # 192.168.1.1,192.168.1.2 + # 192.168.1.1-12 | 192.168.1.1-192.168.1.12 | 192.168.1.0/30 | 192.168.1.1 + ips = [] + ip_list = ip_string.split(',') + if len(ip_list) > 1: + for ip in ip_list: + try: + ips.append(str(IP(ip))) + except ValueError: + pass + return ips + + ip_list = ip_string.split('-') + try: + if len(ip_list) == 2: + start_ip, end_ip = ip_list + if ip_list[1].find('.') == -1: + end_ip = start_ip[:start_ip.rindex('.') + 1] + end_ip + for ip in range(IP(start_ip).int(), IP(end_ip).int() + 1): + ips.extend((str(ip) for ip in IP(ip))) + else: + ips.extend((str(ip) for ip in IP(ip_list[0]))) + except ValueError: + ips = [] + return ips + + +def is_valid_port(port): + valid = True + try: + port = int(port) + if port > 65535 or port < 1: + valid = False + except (TypeError, ValueError): + valid = False + return valid + +def generate_ports(ports): + port_list = [] + if isinstance(ports, int): + port_list.append(ports) + elif isinstance(ports, str): + port_list.extend( + [int(p) for p in ports.split(',') if p.isdigit()] + ) + elif isinstance(ports, list): + port_list = ports + port_list = list(map(int, filter(is_valid_port, port_list))) + return port_list diff --git a/apps/settings/utils/nmap.py b/apps/settings/utils/nmap.py deleted file mode 100644 index 6e4282678..000000000 --- a/apps/settings/utils/nmap.py +++ /dev/null @@ -1,60 +0,0 @@ -import time -import nmap - -from IPy import IP - -from common.utils.timezone import local_now_display - - -def generate_ips(ip_string): - # 支持的格式 - # 192.168.1.1-12 | 192.168.1.1-192.168.1.12 | 192.168.1.0/30 | 192.168.1.1 - ip_list = ip_string.split('-') - ips = [] - try: - if len(ip_list) == 2: - start_ip, end_ip = ip_list - if ip_list[1].find('.') == -1: - end_ip = start_ip[:start_ip.rindex('.') + 1] + end_ip - for ip in range(IP(start_ip).int(), IP(end_ip).int() + 1): - ips.extend(IP(ip)) - else: - ips.extend(IP(ip_list[0])) - except Exception: - ips = [] - return ips - - -def once_nmap(nm, ip, ports, timeout, display): - nmap_version = '.'.join(map(lambda x: str(x), nm.nmap_version())) - display(f'Starting Nmap {nmap_version} at {local_now_display()} for {ip}') - try: - is_ok = True - nm.scan(ip, arguments='-sS -sU -F', ports=ports, timeout=timeout) - tcp_port = nm[ip].get('tcp', {}) - udp_port = nm[ip].get('udp', {}) - display(f'PORT\tSTATE\tSERVICE') - for port, info in tcp_port.items(): - display(f"{port}\t{info.get('state', 'unknown')}\t{info.get('name', 'unknown')}") - for port, info in udp_port.items(): - display(f"{port}\t{info.get('state', 'unknown')}\t{info.get('name', 'unknown')}") - except Exception: - is_ok = False - display(f'Nmap scan report for {ip} error.') - return is_ok - - -def verbose_nmap(dest_ip, dest_port=None, timeout=None, display=print): - dest_port = ','.join(list(dest_port)) if dest_port else None - - ips = generate_ips(dest_ip) - nm = nmap.PortScanner() - success_num, start_time = 0, time.time() - display(f'[Summary] Nmap: {len(ips)} IP addresses were scanned') - for ip in ips: - ok = once_nmap(nm, str(ip), dest_port, timeout, display) - if ok: - success_num += 1 - display('') - display(f'[Done] Nmap: {len(ips)} IP addresses ({success_num} hosts up) ' - f'scanned in {round(time.time() - start_time, 2)} seconds') diff --git a/apps/settings/utils/telnet.py b/apps/settings/utils/telnet.py deleted file mode 100644 index 9cb0a2c0b..000000000 --- a/apps/settings/utils/telnet.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -# -import socket -import telnetlib - -from common.utils import lookup_domain - -PROMPT_REGEX = r'[\<|\[](.*)[\>|\]]' - - -def telnet(dest_addr, port_number=23, timeout=10): - try: - connection = telnetlib.Telnet(dest_addr, port_number, timeout) - except (ConnectionRefusedError, socket.timeout, socket.gaierror) as e: - return False, str(e) - expected_regexes = [bytes(PROMPT_REGEX, encoding='ascii')] - index, prompt_regex, output = connection.expect(expected_regexes, timeout=3) - return True, output.decode('utf-8', 'ignore') - - -def verbose_telnet(dest_ip, dest_port=23, timeout=10, display=None): - if display is None: - display = print - ip = lookup_domain(dest_ip) - if not ip: - return - msg = 'Trying %s (%s:%s)' % (dest_ip, ip, dest_port) - display(msg) - try: - is_connective, resp = telnet(dest_ip, dest_port, timeout) - if is_connective: - template = 'Connected to {0} {1}.\r\n{2}Connection closed by foreign host.' - else: - template = 'telnet: connect to {0} {1} {2}\r\ntelnet: Unable to connect to remote host' - msg = template.format(dest_ip, dest_port, resp) - except Exception as e: - msg = 'Error: %s' % e - display(msg) - - -if __name__ == "__main__": - print(verbose_telnet(dest_addr='1.1.1.1', port_number=2222)) - print(verbose_telnet(dest_addr='baidu.com', port_number=80)) - print(verbose_telnet(dest_addr='baidu.com', port_number=8080)) - print(verbose_telnet(dest_addr='192.168.4.1', port_number=2222)) - print(verbose_telnet(dest_addr='192.168.4.1', port_number=2223)) - print(verbose_telnet(dest_addr='ssssss', port_number=-1)) diff --git a/apps/settings/ws.py b/apps/settings/ws.py index f5752e2a1..b15d61ea7 100644 --- a/apps/settings/ws.py +++ b/apps/settings/ws.py @@ -1,53 +1,73 @@ # -*- coding: utf-8 -*- # - import json -from channels.generic.websocket import JsonWebsocketConsumer +from channels.generic.websocket import AsyncJsonWebsocketConsumer from common.db.utils import close_old_connections from common.utils import get_logger -from .utils import verbose_ping, verbose_telnet, verbose_nmap +from .tools import verbose_ping, verbose_telnet, verbose_nmap, verbose_tcpdump + logger = get_logger(__name__) -class ToolsWebsocket(JsonWebsocketConsumer): +class ToolsWebsocket(AsyncJsonWebsocketConsumer): - def connect(self): + async def connect(self): user = self.scope["user"] if user.is_authenticated: - self.accept() + await self.accept() else: - self.close() + await self.close() - def send_msg(self, msg): - self.send_json({'msg': msg + '\r\n'}) + async def send_msg(self, msg=''): + await self.send_json({'msg': msg + '\r\n'}) - def imitate_ping(self, dest_ip, timeout=3, count=5, psize=64): - """ - Send `count' ping with `psize' size to `dest_ip' with - the given `timeout' and display the result. - """ - logger.info('receive request ping {}'.format(dest_ip)) - verbose_ping(dest_ip, timeout, count, psize, display=self.send_msg) + async def imitate_ping(self, dest_ip, timeout=3, count=5, psize=64): + params = { + 'dest_ip': dest_ip, 'timeout': timeout, + 'count': count, 'psize': psize + } + logger.info(f'Receive request ping: {params}') + await verbose_ping(display=self.send_msg, **params) - def imitate_telnet(self, dest_ip, dest_port=23, timeout=10): - logger.info('receive request telnet {}'.format(dest_ip)) - verbose_telnet(dest_ip, dest_port, timeout, display=self.send_msg) + async def imitate_telnet(self, dest_ip, dest_port=23, timeout=10): + params = { + 'dest_ip': dest_ip, 'dest_port': dest_port, 'timeout': timeout, + } + logger.info(f'Receive request telnet: {params}') + await verbose_telnet(display=self.send_msg, **params) - def imitate_nmap(self, dest_ip, dest_port=None, timeout=None): - logger.info('receive request nmap {}'.format(dest_ip)) - verbose_nmap(dest_ip, dest_port, timeout, display=self.send_msg) + async def imitate_nmap(self, dest_ips, dest_ports=None, timeout=None): + params = { + 'dest_ips': dest_ips, 'dest_ports': dest_ports, 'timeout': timeout, + } + logger.info(f'Receive request nmap: {params}') + await verbose_nmap(display=self.send_msg, **params) - def receive(self, text_data=None, bytes_data=None, **kwargs): + async def imitate_tcpdump( + self, interfaces=None, src_ips='', + src_ports='', dest_ips='', dest_ports='' + ): + params = { + 'interfaces': interfaces, 'src_ips': src_ips, 'src_ports': src_ports, + 'dest_ips': dest_ips, 'dest_ports': dest_ports + } + logger.info(f'Receive request tcpdump: {params}') + await verbose_tcpdump(display=self.send_msg, **params) + + async def receive(self, text_data=None, bytes_data=None, **kwargs): data = json.loads(text_data) tool_type = data.pop('tool_type', 'Ping') + try: + tool_func = getattr(self, f'imitate_{tool_type.lower()}') + await tool_func(**data) + except Exception as error: + await self.send_msg('Exception: %s' % error) + await self.send_msg() + await self.close() - tool_func = getattr(self, f'imitate_{tool_type.lower()}') - tool_func(**data) - self.close() - - def disconnect(self, code): - self.close() + async def disconnect(self, code): + await self.close() close_old_connections() diff --git a/pyproject.toml b/pyproject.toml index 3df29cc7d..860e65003 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,6 +140,7 @@ pympler = "1.0.1" hvac = "1.1.1" pyhcl = "0.4.4" ipy = "1.1" +netifaces = "^0.11.0" [tool.poetry.group.dev.dependencies] diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 221421582..50b2fea41 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -94,6 +94,7 @@ openapi-codec==1.3.2 Pillow==10.0.0 pytz==2023.3 # Runtime +netifaces==0.11.0 django-proxy==1.2.2 channels-redis==4.1.0 python-daemon==3.0.1