feat: 系统工具改为异步,增加tcpdump工具

pull/11210/head
jiangweidong 2023-08-07 09:37:24 +08:00 committed by Bryan
parent 8accd296b8
commit 7636255533
13 changed files with 358 additions and 165 deletions

View File

@ -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}'

View File

@ -0,0 +1,6 @@
# coding: utf-8
#
from .ping import *
from .telnet import *
from .nmap import *
from .tcpdump import *

View File

@ -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')

View File

@ -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__":

View File

@ -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()

View File

@ -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))

View File

@ -3,6 +3,3 @@
from .ldap import *
from .common import *
from .ping import *
from .telnet import *
from .nmap import *

View File

@ -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

View File

@ -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')

View File

@ -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))

View File

@ -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()

View File

@ -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]

View File

@ -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