diff --git a/chaos_monkey/network/info.py b/chaos_monkey/network/info.py index 59b40a089..605799ce3 100644 --- a/chaos_monkey/network/info.py +++ b/chaos_monkey/network/info.py @@ -1,81 +1,56 @@ import os import sys -import array import socket import struct import psutil import ipaddress +import itertools +import netifaces from subprocess import check_output from random import randint -if sys.platform == "win32": - import netifaces +def get_host_subnets(): + """ + Returns a list of subnets visible to host (omitting loopback and auto conf networks) + Each subnet item contains the host IP in that network + the subnet. + :return: List of dict, keys are "addr" and "subnet" + """ + ipv4_nets = [netifaces.ifaddresses(interface)[netifaces.AF_INET] + for interface in netifaces.interfaces() + if netifaces.AF_INET in netifaces.ifaddresses(interface) + ] + # flatten + ipv4_nets = itertools.chain.from_iterable(ipv4_nets) + # remove loopback + ipv4_nets = [network for network in ipv4_nets if network['addr'] != '127.0.0.1'] + # remove auto conf + ipv4_nets = [network for network in ipv4_nets if not network['addr'].startswith('169.254')] + for network in ipv4_nets: + if 'broadcast' in network: + network.pop('broadcast') + return ipv4_nets + + +if sys.platform == "win32": def local_ips(): local_hostname = socket.gethostname() return socket.gethostbyname_ex(local_hostname)[2] - def get_host_subnets(only_ips=False): - network_adapters = [] - valid_ips = local_ips() - if only_ips: - return valid_ips - interfaces = [netifaces.ifaddresses(x) for x in netifaces.interfaces()] - for inte in interfaces: - if netifaces.AF_INET in inte: - for add in inte[netifaces.AF_INET]: - if "netmask" in add and add["addr"] in valid_ips: - network_adapters.append((add["addr"], add["netmask"])) - return network_adapters - def get_routes(): raise NotImplementedError() else: from fcntl import ioctl - def get_host_subnets(only_ips=False): - """Get the list of Linux network adapters.""" - max_bytes = 8096 - is_64bits = sys.maxsize > 2 ** 32 - if is_64bits: - offset1 = 16 - offset2 = 40 - else: - offset1 = 32 - offset2 = 32 - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - names = array.array('B', '\0' * max_bytes) - outbytes = struct.unpack('iL', ioctl( - sock.fileno(), - 0x8912, - struct.pack('iL', max_bytes, names.buffer_info()[0])))[0] - adapter_names = [names.tostring()[n_cnt:n_cnt + offset1].split('\0', 1)[0] - for n_cnt in xrange(0, outbytes, offset2)] - network_adapters = [] - for adapter_name in adapter_names: - ip_address = socket.inet_ntoa(ioctl( - sock.fileno(), - 0x8915, - struct.pack('256s', adapter_name))[20:24]) - if ip_address.startswith('127'): - continue - subnet_mask = socket.inet_ntoa(ioctl( - sock.fileno(), - 0x891b, - struct.pack('256s', adapter_name))[20:24]) - - if only_ips: - network_adapters.append(ip_address) - else: - network_adapters.append((ip_address, subnet_mask)) - - return network_adapters def local_ips(): - return get_host_subnets(only_ips=True) + ipv4_nets = get_host_subnets() + valid_ips = [network['addr'] for network in ipv4_nets] + return valid_ips + def get_routes(): # based on scapy implementation for route parsing LOOPBACK_NAME = "lo" @@ -141,6 +116,11 @@ def get_free_tcp_port(min_range=1000, max_range=65535): def check_internet_access(services): + """ + Checks if any of the services are accessible, over ICMP + :param services: List of IPs/hostnames + :return: boolean depending on internet access + """ ping_str = "-n 1" if sys.platform.startswith("win") else "-c 1" for host in services: if os.system("ping " + ping_str + " " + host) == 0: @@ -149,19 +129,24 @@ def check_internet_access(services): def get_ips_from_interfaces(): + """ + Returns a list of IPs accessible in the host in each network interface, in the subnet. + Limits to a single class C if the network is larger + :return: List of IPs, marked as strings. + """ res = [] ifs = get_host_subnets() - for interface in ifs: - ipint = ipaddress.ip_interface(u"%s/%s" % interface) + for net_interface in ifs: + host_addr = ipaddress.ip_address(net_interface['addr']) + ip_interface = ipaddress.ip_interface(u"%s/%s" % (net_interface['addr'], net_interface['netmask'])) # limit subnet scans to class C only - if ipint.network.num_addresses > 255: - ipint = ipaddress.ip_interface(u"%s/24" % interface[0]) - for addr in ipint.network.hosts(): - if str(addr) == interface[0]: - continue - res.append(str(addr)) + if ip_interface.network.num_addresses > 255: + ip_interface = ipaddress.ip_interface(u"%s/24" % net_interface['addr']) + addrs = [str(addr) for addr in ip_interface.network.hosts() if addr != host_addr] + res.extend(addrs) return res + if sys.platform == "win32": def get_ip_for_connection(target_ip): return None @@ -171,7 +156,7 @@ else: query_str = 'ip route get %s' % target_ip resp = check_output(query_str.split()) substr = resp.split() - src = substr[substr.index('src')+1] + src = substr[substr.index('src') + 1] return src except Exception: - return None \ No newline at end of file + return None diff --git a/chaos_monkey/system_info/__init__.py b/chaos_monkey/system_info/__init__.py index 2238fb542..b9a16d459 100644 --- a/chaos_monkey/system_info/__init__.py +++ b/chaos_monkey/system_info/__init__.py @@ -2,6 +2,7 @@ import sys import socket import psutil from enum import IntEnum +from network.info import get_host_subnets, local_ips __author__ = 'uri' @@ -45,9 +46,19 @@ class InfoCollector(object): self.info = {} def get_hostname(self): - self.info['hostname'] = socket.gethostname() + """ + Adds the fully qualified computer hostname to the system information. + :return: Nothing + """ + self.info['hostname'] = socket.getfqdn() def get_process_list(self): + """ + Adds process information from the host to the system information. + Currently lists process name, ID, parent ID, command line + and the full image path of each process. + :return: Nothing + """ processes = {} for process in psutil.process_iter(): try: @@ -69,3 +80,12 @@ class InfoCollector(object): } pass self.info['process_list'] = processes + + def get_network_info(self): + """ + Adds network information from the host to the system information. + Currently updates with a list of networks accessible from host, + containing host ip and the subnet range. + :return: None + """ + self.info['network_info'] = {'networks': get_host_subnets()} diff --git a/chaos_monkey/system_info/linux_info_collector.py b/chaos_monkey/system_info/linux_info_collector.py index 64a31c865..6c7570fc0 100644 --- a/chaos_monkey/system_info/linux_info_collector.py +++ b/chaos_monkey/system_info/linux_info_collector.py @@ -14,4 +14,5 @@ class LinuxInfoCollector(InfoCollector): def get_info(self): self.get_hostname() self.get_process_list() + self.get_network_info() return self.info diff --git a/chaos_monkey/system_info/windows_info_collector.py b/chaos_monkey/system_info/windows_info_collector.py index 5cb1253ab..2ba26fd34 100644 --- a/chaos_monkey/system_info/windows_info_collector.py +++ b/chaos_monkey/system_info/windows_info_collector.py @@ -14,6 +14,7 @@ class WindowsInfoCollector(InfoCollector): def get_info(self): self.get_hostname() self.get_process_list() + self.get_network_info() mimikatz_collector = MimikatzCollector() self.info["credentials"] = mimikatz_collector.get_logon_info() return self.info