diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 61cfbcea..12fec791 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,11 @@ +Version 3.0 (alpha) +----------- +* Migrate diverters, listeners and other components to Python 3 +* Retire BITS listener for now +* Fix hard-coded FakeNet path for linux platform in test.py +* Update README and developement documentation +* Add copyright notice to modified files + Version 1.4.13 -------------- * Port test scripts to Python 3 diff --git a/README.md b/README.md index 83f1b1c3..0e8be9e2 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,3 @@ -Development Suspended -===================== - -The FLARE Team must suspend development and maintenance of FakeNet-NG for the -time being. - -FLARE has opted to indicate the project status here instead of archiving the -project. This will allow users and maintainers to continue to log issues -documenting valuable information about problems, troubleshooting, and -work-arounds. - -Original Documentation Follows -============================== ______ _ ________ _ _ ______ _______ _ _ _____ | ____/\ | |/ / ____| \ | | ____|__ __| | \ | |/ ____| | |__ / \ | ' /| |__ | \| | |__ | |______| \| | | __ @@ -20,7 +7,7 @@ Original Documentation Follows D O C U M E N T A T I O N -FakeNet-NG is a next generation dynamic network analysis tool for malware +FakeNet-NG 3.0 (alpha) is a next generation dynamic network analysis tool for malware analysts and penetration testers. It is open source and designed for the latest versions of Windows (and Linux, for certain modes of operation). FakeNet-NG is based on the excellent Fakenet tool developed by Andrew Honig and Michael @@ -57,10 +44,6 @@ analysis machine. Installing module ----------------- - -Installation on Windows requires the following dependency: - * [Microsoft Visual C++ Compiler for Python 2.7](https://aka.ms/vcpython27) - Installation on Linux requires the following dependencies: * Python pip package manager (e.g. python-pip for Ubuntu). * Python development files (e.g. python-dev for Ubuntu). @@ -94,7 +77,7 @@ Finally if you would like to avoid installing FakeNet-NG and just want to run it as-is (e.g. for development), then you would need to obtain the source code and install dependencies as follows: -1) Install 64-bit or 32-bit Python 2.7.x for the 64-bit or 32-bit versions +1) Install 64-bit or 32-bit Python 3.7.x for the 64-bit or 32-bit versions of Windows respectively. 2) Install Python dependencies: @@ -116,7 +99,7 @@ install dependencies as follows: Execute FakeNet-NG by running it with a Python interpreter in a privileged shell: - python fakenet.py + python -m fakenet.fakenet Usage ===== @@ -133,12 +116,12 @@ parameter to get simple help: | | / ____ \| . \| |____| |\ | |____ | | | |\ | |__| | |_|/_/ \_\_|\_\______|_| \_|______| |_| |_| \_|\_____| - Version 1.0 + Version 3.0 (alpha) _____________________________________________________________ Developed by FLARE Team - Copyright (C) 2016-2022 Mandiant, Inc. All rights reserved. + Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. _____________________________________________________________ - Usage: fakenet.py [options]: + Usage: python -m fakenet.fakenet [options]: Options: -h, --help show this help message and exit @@ -188,7 +171,7 @@ and an HTTP connection: | | / ____ \| . \| |____| |\ | |____ | | | |\ | |__| | |_|/_/ \_\_|\_\______|_| \_|______| |_| |_| \_|\_____| - Version 1.0 + Version 3.0 (alpha) _____________________________________________________________ Developed by FLARE Team Copyright (C) 2016-2022 Mandiant, Inc. All rights reserved. diff --git a/docs/srs.md b/docs/srs.md index 5d58b58b..89333cdf 100644 --- a/docs/srs.md +++ b/docs/srs.md @@ -116,7 +116,7 @@ currently implemented in any FakeNet-NG release), and `Auto`. ### Configuration Settings and Sections The configuration file used on each platform must conform to the format used by the others. As of this writing, it is based on the Python -[ConfigParser](https://docs.python.org/2/library/configparser.html) package, +[ConfigParser](https://docs.python.org/3/library/configparser.html) package, which is similar to an INI file. ### Quick Glossary of Miscellaneous Terms diff --git a/docs/testing.md b/docs/testing.md index 601b10d1..5afcf6cd 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -127,7 +127,7 @@ into `pytest`, that is fine, but the current script serves. The salient peculiarity of `test.py` is that on Windows it requires you to install FakeNet and it then runs the global `fakenet.exe` script entry point available via the Windows path environment variable; and on Linux, it executes -`python fakenet.py` directly. Alas, this is for purely undocumented and +`python -m fakenet.fakenet` directly. Alas, this is for purely undocumented and forgettable reasons. It shouldn't be too difficult to make these consistent if that becomes important to someone. @@ -210,7 +210,8 @@ modes, and clients: In each combination, FakeNet must be tested against the full range of applicable tests in the Automated Test Suite provided by the test script -`test/test.py`. +`test/test.py`. It should be noted that some tests are expected to output +errors such as socket errors even when the tests are successful. As of this writing, the Manual Test Suite must also be executed if the FakeNet feature set is to be fully exercised for quality assurance of a given release. diff --git a/fakenet/configs/CustomProviderExample.py b/fakenet/configs/CustomProviderExample.py index ae928cd6..0a3c84f2 100644 --- a/fakenet/configs/CustomProviderExample.py +++ b/fakenet/configs/CustomProviderExample.py @@ -1,3 +1,5 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import socket # To read about customizing HTTP responses, see docs/CustomResponse.md @@ -14,7 +16,7 @@ def HandleRequest(req, method, post_data=None): The HTTP post data received by calling `rfile.read()` against the BaseHTTPRequestHandler that received the request. """ - response = 'Ahoy\r\n' + response = b'Ahoy\r\n' if method == 'GET': req.send_response(200) @@ -51,8 +53,8 @@ def HandleTcp(sock): if not data: break - resp = raw_input('\nEnter a response for the TCP client: ') - sock.sendall(resp) + resp = input('\nEnter a response for the TCP client: ') + sock.sendall(resp.encode()) def HandleUdp(sock, data, addr): @@ -68,5 +70,5 @@ def HandleUdp(sock, data, addr): The host and port of the remote peer """ if data: - resp = raw_input('\nEnter a response for the UDP client: ') - sock.sendto(resp, addr) + resp = input('\nEnter a response for the UDP client: ') + sock.sendto(resp.encode(), addr) diff --git a/fakenet/configs/default.ini b/fakenet/configs/default.ini index b64d0219..9351b19d 100644 --- a/fakenet/configs/default.ini +++ b/fakenet/configs/default.ini @@ -178,7 +178,6 @@ BlackListPortsUDP: 67, 68, 137, 138, 443, 1900, 5355 # * Webroot - Set webroot path for HTTPListener. # * DumpHTTPPosts - Store HTTP Post requests for the HTTPListener. # * DumpHTTPPostsFilePrefix - File prefix for the stored HTTP Post requests used by the HTTPListener. -# * BITSFilePrefix - File prefix for the stored BITS uploads used by the BITSListener. # * TFTPFilePrefix - File prefix for the stored tftp uploads used by the TFTPListener. # * DNSResponse - IP address to respond with for A record DNS queries. (DNSListener) # * NXDomains - A number of DNS requests to ignore to let the malware cycle through @@ -206,7 +205,7 @@ Enabled: True Protocol: TCP Listener: ProxyListener Port: 38926 -Listeners: HTTPListener, RawListener, FTPListener, DNSListener, POPListener, SMTPListener, TFTPListener, IRCListener, BITSListener +Listeners: HTTPListener, RawListener, FTPListener, DNSListener, POPListener, SMTPListener, TFTPListener, IRCListener Hidden: False [ProxyUDPListener] diff --git a/fakenet/diverters/debuglevels.py b/fakenet/diverters/debuglevels.py index 535dea94..f1dc8074 100644 --- a/fakenet/diverters/debuglevels.py +++ b/fakenet/diverters/debuglevels.py @@ -1,3 +1,5 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + # Debug print levels for fine-grained debug trace output control DNFQUEUE = (1 << 0) # netfilterqueue DGENPKT = (1 << 1) # Generic packet handling @@ -39,4 +41,4 @@ DMISC: 'MISC', } -DLABELS_INV = {v.upper(): k for k, v in DLABELS.iteritems()} +DLABELS_INV = {v.upper(): k for k, v in DLABELS.items()} diff --git a/fakenet/diverters/diverterbase.py b/fakenet/diverters/diverterbase.py index dbfb3341..521040da 100644 --- a/fakenet/diverters/diverterbase.py +++ b/fakenet/diverters/diverterbase.py @@ -1,3 +1,5 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import os import abc import sys @@ -10,7 +12,7 @@ import subprocess from . import fnpacket from . import fnconfig -from debuglevels import * +from .debuglevels import * from collections import namedtuple from collections import OrderedDict @@ -92,7 +94,7 @@ def first_packet_new_session(self): (self.pkt.dst_ip, self.pkt.dport)) -class DiverterPerOSDelegate(object): +class DiverterPerOSDelegate(object, metaclass=abc.ABCMeta): """Delegate class for OS-specific methods that FakeNet-NG implementors must override. @@ -105,7 +107,6 @@ class DiverterPerOSDelegate(object): check_gateways (currently only a warning) check_dns_servers (currently only a warning) """ - __metaclass__ = abc.ABCMeta @abc.abstractmethod def check_active_ethernet_adapters(self): @@ -332,7 +333,7 @@ def isHidden(self, proto, port): def getPortList(self, proto): if proto in self.protos: - return self.protos[proto].keys() + return list(self.protos[proto].keys()) return [] def intersectsWithPorts(self, proto, ports): @@ -540,7 +541,7 @@ def __init__(self, diverter_config, listeners_config, ip_addrs, stringlists = ['HostBlackList'] self.configure(diverter_config, portlists, stringlists) self.listeners_config = dict((k.lower(), v) - for k, v in listeners_config.iteritems()) + for k, v in listeners_config.items()) # Local IP address self.external_ip = socket.gethostbyname(socket.gethostname()) @@ -773,7 +774,7 @@ def parse_listeners_config(self, listeners_config): ####################################################################### # Populate diverter ports and process filters from the configuration - for listener_name, listener_config in listeners_config.iteritems(): + for listener_name, listener_config in listeners_config.items(): if 'port' in listener_config: @@ -1238,6 +1239,12 @@ def formatPkt(self, pkt, pid, comm): Returns: A str containing the log line """ + if pid == None: + pid = 'None' + + if comm == None: + comm = 'None' + logline = '' if pkt.proto == 'UDP': diff --git a/fakenet/diverters/fnconfig.py b/fakenet/diverters/fnconfig.py index 5a970617..8379676d 100644 --- a/fakenet/diverters/fnconfig.py +++ b/fakenet/diverters/fnconfig.py @@ -1,3 +1,5 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + class Config(object): """Configuration primitives. @@ -19,7 +21,7 @@ def configure(self, config_dict, portlists=[], stringlists=[]): 2.) Turn string lists into arrays for quicker access 3.) Expand port range specifications """ - self._dict = dict((k.lower(), v) for k, v in config_dict.iteritems()) + self._dict = dict((k.lower(), v) for k, v in config_dict.items()) for entry in portlists: portlist = self.getconfigval(entry) @@ -51,8 +53,8 @@ def _expand_ports(self, ports_list): if '-' not in i: ports.append(int(i)) else: - l, h = map(int, i.split('-')) - ports += range(l, h + 1) + l, h = list(map(int, i.split('-'))) + ports += list(range(l, h + 1)) return ports def _fuzzy_true(self, value): @@ -62,7 +64,7 @@ def _fuzzy_false(self, value): return value.lower() in ['no', 'off', 'false', 'disable', 'disabled'] def is_configured(self, opt): - return opt.lower() in self._dict.keys() + return opt.lower() in list(self._dict.keys()) def is_unconfigured(self, opt): return not self.is_configured(opt) diff --git a/fakenet/diverters/fnpacket.py b/fakenet/diverters/fnpacket.py index 91355c1b..f45615bb 100644 --- a/fakenet/diverters/fnpacket.py +++ b/fakenet/diverters/fnpacket.py @@ -1,7 +1,9 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import dpkt import socket import logging -import debuglevels +from . import debuglevels class PacketCtx(object): @@ -64,7 +66,7 @@ def __init__(self, label, raw): self.dkey = None # Parse as much as possible - self.ipver = ((ord(self._raw[0]) & 0xf0) >> 4) + self.ipver = ((self._raw[0] & 0xf0) >> 4) if self.ipver == 4: self._parseIpv4() elif self.ipver == 6: diff --git a/fakenet/diverters/linutil.py b/fakenet/diverters/linutil.py index 21aff993..fbdb1289 100644 --- a/fakenet/diverters/linutil.py +++ b/fakenet/diverters/linutil.py @@ -1,3 +1,5 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import os import re import glob @@ -8,7 +10,7 @@ import threading import subprocess import netfilterqueue -from debuglevels import * +from .debuglevels import * from collections import defaultdict from . import diverterbase @@ -263,7 +265,7 @@ class LinUtilMixin(diverterbase.DiverterPerOSDelegate): def init_linux_mixin(self): self.old_dns = None - self.iptables_captured = '' + self.iptables_captured = b'' def getNewDestinationIp(self, ip): """On Linux, FTP tests fail if IP redirection uses the external IP, so @@ -294,18 +296,18 @@ def fix_dns(self): return False def linux_capture_iptables(self): - self.iptables_captured = '' + self.iptables_captured = b'' ret = None try: p = subprocess.Popen(['iptables-save'], stdout=subprocess.PIPE) while True: buf = p.stdout.read() - if buf == '': + if buf == b'': break self.iptables_captured += buf - if self.iptables_captured == '': + if self.iptables_captured == b'': self.logger.warning('Null iptables-save output, likely not ' + 'privileged') ret = p.wait() @@ -397,7 +399,7 @@ def linux_get_next_nfqueue_numbers(self, n): existing_queues = self.linux_get_current_nfnlq_bindings() next_qnos = list() - for qno in xrange(QNO_MAX + 1): + for qno in range(QNO_MAX + 1): if qno not in existing_queues: next_qnos.append(qno) if len(next_qnos) == n: diff --git a/fakenet/diverters/linux.py b/fakenet/diverters/linux.py index 48e4add2..8591c7d3 100644 --- a/fakenet/diverters/linux.py +++ b/fakenet/diverters/linux.py @@ -1,3 +1,5 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import sys import dpkt import time @@ -7,10 +9,10 @@ import threading import subprocess import netfilterqueue -from linutil import * +from .linutil import * from . import fnpacket -from debuglevels import * -from diverterbase import * +from .debuglevels import * +from .diverterbase import * from collections import namedtuple from netfilterqueue import NetfilterQueue diff --git a/fakenet/diverters/windows.py b/fakenet/diverters/windows.py index a72f1012..ef24a5b8 100644 --- a/fakenet/diverters/windows.py +++ b/fakenet/diverters/windows.py @@ -1,3 +1,5 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + # Diverter for Windows implemented using WinDivert library import logging @@ -14,8 +16,8 @@ import threading import platform -from winutil import * -from diverterbase import * +from .winutil import * +from .diverterbase import * import subprocess @@ -127,7 +129,7 @@ def __init__(self, diverter_config, listeners_config, ip_addrs, try: self.handle = WinDivert(filter=self.filter) self.handle.open() - except WindowsError, e: + except WindowsError as e: if e.winerror == 5: self.logger.critical('ERROR: Insufficient privileges to run ' 'windows diverter.') @@ -196,7 +198,7 @@ def divert_thread(self): self.setLastErrorNull() # WinDivert/LastError workaround try: self.handle.send(pkt.wdpkt) - except Exception, e: + except Exception as e: protocol = 'Unknown' @@ -234,7 +236,7 @@ def stopCallback(self): subprocess.check_call(cmd_set_dhcp, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - except subprocess.CalledProcessError, e: + except subprocess.CalledProcessError as e: self.logger.error('Failed to restore DHCP on interface %s.' % interface_name) else: @@ -257,7 +259,7 @@ def stopCallback(self): subprocess.check_call(cmd_set_dns_dhcp, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - except subprocess.CalledProcessError, e: + except subprocess.CalledProcessError as e: self.logger.error("Failed to restore DNS on interface %s." % interface_name) else: diff --git a/fakenet/diverters/winutil.py b/fakenet/diverters/winutil.py index 36202891..ab83fe50 100644 --- a/fakenet/diverters/winutil.py +++ b/fakenet/diverters/winutil.py @@ -1,3 +1,5 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + #!/usr/bin/env python import logging logging.basicConfig(format='%(asctime)s [%(name)18s] %(message)s', @@ -15,7 +17,7 @@ import time -from _winreg import * +from winreg import * import subprocess @@ -379,7 +381,7 @@ def fix_gateway(self): subprocess.check_call(cmd_set_gw, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - except subprocess.CalledProcessError, e: + except subprocess.CalledProcessError as e: self.logger.error(" Failed to set gateway %s on interface %s." % (gw_address, interface_name)) else: @@ -416,13 +418,14 @@ def fix_dns(self): # Configure DNS server try: - subprocess.check_call(cmd_set_dns, + subprocess.check_output(cmd_set_dns, shell=True, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - except subprocess.CalledProcessError, e: + except subprocess.CalledProcessError as e: self.logger.error(" Failed to set DNS %s on interface %s." % (dns_address, interface_name)) + self.logger.error(" netsh failed with error: %s" + % (e.output)) else: self.logger.info(" Setting DNS %s on interface %s" % (dns_address, interface_name)) @@ -446,7 +449,7 @@ def check_gateways(self): for adapter in self.get_adapters_info(): for gateway in self.get_gateways(adapter): - if gateway != '0.0.0.0': + if gateway != b'0.0.0.0': return True else: return False @@ -530,7 +533,7 @@ def open_service(self, sc_handle, service_name, dwDesiredAccess) if service_handle == 0: - self.logger.error('Failed to call OpenService') + self.logger.error('OpenService failed for %s', service_name) return return service_handle @@ -650,7 +653,7 @@ def start_service_helper(self, service_name='Dnscache'): service_name, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - except subprocess.CalledProcessError, e: + except subprocess.CalledProcessError as e: self.logger.error( 'Failed to enable the service %s. (sc config)', service_name) @@ -700,7 +703,7 @@ def start_service_helper(self, service_name='Dnscache'): subprocess.check_call("net start %s" % service_name, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - except subprocess.CalledProcessError, e: + except subprocess.CalledProcessError as e: self.logger.error( 'Failed to start the service %s. (net stop)', service_name) else: @@ -739,7 +742,7 @@ def stop_service_helper(self, service_name='Dnscache'): service_name, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - except subprocess.CalledProcessError, e: + except subprocess.CalledProcessError as e: self.logger.error( 'Failed to disable the service %s. (sc config)', service_name) else: @@ -788,7 +791,7 @@ def stop_service_helper(self, service_name='Dnscache'): subprocess.check_call("net stop %s" % service_name, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - except subprocess.CalledProcessError, e: + except subprocess.CalledProcessError as e: self.logger.error( 'Failed to stop the service %s. (net stop)', service_name) else: @@ -902,7 +905,8 @@ def get_process_image_filename(self, pid): lpImageFileName = create_string_buffer(MAX_PATH) if windll.psapi.GetProcessImageFileNameA(hProcess, lpImageFileName, MAX_PATH) > 0: - process_name = os.path.basename(lpImageFileName.value) + # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/3f6cc0e2-1303-4088-a26b-fb9582f29197 + process_name = os.path.basename(lpImageFileName.value.decode("utf-8")) else: self.logger.error('Failed to call GetProcessImageFileNameA, %d' % (ctypes.GetLastError())) @@ -1033,7 +1037,7 @@ def get_ipaddresses(self, adapter): while ipaddress: - yield ipaddress.IpAddress.String + yield ipaddress.IpAddress.String.decode("utf-8") ipaddress = ipaddress.Next def get_ipaddresses_netmask(self, adapter): @@ -1042,7 +1046,7 @@ def get_ipaddresses_netmask(self, adapter): while ipaddress: - yield (ipaddress.IpAddress.String, ipaddress.IpMask.String) + yield (ipaddress.IpAddress.String.decode("utf-8"), ipaddress.IpMask.String.decode("utf-8")) ipaddress = ipaddress.Next def get_ipaddresses_index(self, index): @@ -1057,7 +1061,7 @@ def get_ip_with_gateway(self): for adapter in self.get_adapters_info(): for gateway in self.get_gateways(adapter): if gateway != '0.0.0.0': - return self.get_ipaddresses(adapter).next() + return next(self.get_ipaddresses(adapter)) else: return None @@ -1200,7 +1204,7 @@ def flush_dns(self): try: subprocess.check_call( 'ipconfig /flushdns', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - except subprocess.CalledProcessError, e: + except subprocess.CalledProcessError as e: self.logger.error("Failed to flush DNS cache. Local machine may " "use cached DNS results.") else: @@ -1244,25 +1248,25 @@ def set_dns_server(self, dns_server='127.0.0.1'): value = 'NameServer' for adapter in self.get_active_ethernet_adapters(): - + adapter_name = adapter.AdapterName.decode("utf-8") # Preserve existing setting dns_server_backup = self.get_reg_value(key, sub_key % - adapter.AdapterName, value) + adapter_name, value) # Restore previous value or a blank string if the key was not # present if dns_server_backup: - self.adapters_dns_server_backup[adapter.AdapterName] = ( + self.adapters_dns_server_backup[adapter_name] = ( dns_server_backup, adapter.FriendlyName) else: - self.adapters_dns_server_backup[adapter.AdapterName] = ( + self.adapters_dns_server_backup[adapter_name] = ( '', adapter.FriendlyName) # Set new dns server value - if self.set_reg_value(key, sub_key % adapter.AdapterName, value, dns_server): - self.logger.debug('Set DNS server %s on the adapter: %s', + if self.set_reg_value(key, sub_key % adapter_name, value, dns_server): + self.logger.error('Set DNS server %s on the adapter: %s', dns_server, adapter.FriendlyName) - self.notify_ip_change(adapter.AdapterName) + self.notify_ip_change(adapter_name) else: self.logger.error( 'Failed to set DNS server %s on the adapter: %s', dns_server, adapter.FriendlyName) @@ -1339,7 +1343,7 @@ def __init__(self, name='WinUtil'): for adapter in self.get_active_ethernet_adapters(): self.logger.info('active ethernet index: %s friendlyname: %s name: %s', - adapter.IfIndex, adapter.FriendlyName, adapter.AdapterName) + adapter.IfIndex, adapter.FriendlyName, adapter.AdapterName.decode('utf-8')) def test_registry_nameserver(): diff --git a/fakenet/fakenet.py b/fakenet/fakenet.py index 8d0c3f03..c395b977 100644 --- a/fakenet/fakenet.py +++ b/fakenet/fakenet.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2022 Mandiant, Inc. All rights reserved. +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. #!/usr/bin/env python # @@ -20,7 +20,7 @@ from collections import OrderedDict from optparse import OptionParser,OptionGroup -from ConfigParser import ConfigParser +from configparser import ConfigParser import platform @@ -29,8 +29,8 @@ ############################################################################### # Listener services -import listeners -from listeners import * +from fakenet import listeners +from fakenet.listeners import * ############################################################################### # FakeNet @@ -62,14 +62,19 @@ def __init__(self, logging_level = logging.INFO): self.running_listener_providers = list() def parse_config(self, config_filename): + # Handling Pyinstaller bundle scenario: https://pyinstaller.org/en/stable/runtime-information.html + if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): + dir_path = os.getcwd() + else: + dir_path = os.path.dirname(__file__) if not config_filename: - config_filename = os.path.join(os.path.dirname(__file__), 'configs', 'default.ini') + config_filename = os.path.join(dir_path, 'configs', 'default.ini') if not os.path.exists(config_filename): - config_filename = os.path.join(os.path.dirname(__file__), 'configs', config_filename) + config_filename = os.path.join(dir_path, 'configs', config_filename) if not os.path.exists(config_filename): @@ -105,8 +110,8 @@ def expand_ports(self, ports_list): if '-' not in i: ports.append(int(i)) else: - l,h = map(int, i.split('-')) - ports+= range(l,h+1) + l,h = list(map(int, i.split('-'))) + ports+= list(range(l,h+1)) return ports def expand_listeners(self, listeners_config): @@ -166,7 +171,7 @@ def start(self): if self.diverter_config['networkmode'].lower() == 'auto': self.diverter_config['networkmode'] = 'singlehost' - from diverters.windows import Diverter + from fakenet.diverters.windows import Diverter self.diverter = Diverter(self.diverter_config, self.listeners_config, ip_addrs, self.logging_level) elif platform_name.lower().startswith('linux'): @@ -192,7 +197,7 @@ def start(self): fn_iface) sys.exit(1) - from diverters.linux import Diverter + from fakenet.diverters.linux import Diverter self.diverter = Diverter(self.diverter_config, self.listeners_config, ip_addrs, self.logging_level) else: @@ -325,7 +330,7 @@ def get_ips(self, ipvers, iface=None): def main(): - print """ + print(""" ______ _ ________ _ _ ______ _______ _ _ _____ | ____/\ | |/ / ____| \ | | ____|__ __| | \ | |/ ____| | |__ / \ | ' /| |__ | \| | |__ | |______| \| | | __ @@ -333,15 +338,15 @@ def main(): | | / ____ \| . \| |____| |\ | |____ | | | |\ | |__| | |_|/_/ \_\_|\_\______|_| \_|______| |_| |_| \_|\_____| - Version 1.4.13 + Version 3.0 (alpha) _____________________________________________________________ Developed by FLARE Team Copyright (C) 2016-2022 Mandiant, Inc. All rights reserved. _____________________________________________________________ - """ + """) # Parse command line arguments - parser = OptionParser(usage = "fakenet.py [options]:") + parser = OptionParser(usage = "python -m fakenet.fakenet [options]:") parser.add_option("-c", "--config-file", action="store", dest="config_file", help="configuration filename", metavar="FILE") parser.add_option("-v", "--verbose", @@ -378,7 +383,7 @@ def main(): loghandler = logging.StreamHandler(stream=open(options.log_file, 'a')) except IOError: - print('Failed to open specified log file: %s' % (options.log_file)) + print(('Failed to open specified log file: %s' % (options.log_file))) sys.exit(1) loghandler.formatter = logging.Formatter( '%(asctime)s [%(name)18s] %(message)s', datefmt=date_format) @@ -392,8 +397,8 @@ def main(): elif platform_name.lower().startswith('linux'): sysloghandler = logging.handlers.SysLogHandler('/dev/log') else: - print('Error: Your system %s is currently not supported.' % - (platform_name)) + print(('Error: Your system %s is currently not supported.' % + (platform_name))) sys.exit(1) # Specify datefmt for consistency, but syslog generally logs the time diff --git a/fakenet/listeners/BITSListener.py b/fakenet/listeners/BITSListener.py deleted file mode 100644 index a64ed50d..00000000 --- a/fakenet/listeners/BITSListener.py +++ /dev/null @@ -1,542 +0,0 @@ -# Based on a simple BITS server by Dor Azouri - -import logging - -import os -import sys - -import threading -import SocketServer -import BaseHTTPServer - -import ssl -import socket - -import posixpath - -import time - -import urllib - -from BaseHTTPServer import HTTPServer -from SimpleHTTPServer import SimpleHTTPRequestHandler - -from . import * - -INDENT = ' ' - -# BITS Protocol header keys -K_BITS_SESSION_ID = 'BITS-Session-Id' -K_BITS_ERROR_CONTEXT = 'BITS-Error-Context' -K_BITS_ERROR_CODE = 'BITS-Error-Code' -K_BITS_PACKET_TYPE = 'BITS-Packet-Type' -K_BITS_SUPPORTED_PROTOCOLS = 'BITS-Supported-Protocols' -K_BITS_PROTOCOL = 'BITS-Protocol' - -# HTTP Protocol header keys -K_ACCEPT_ENCODING = 'Accept-Encoding' -K_CONTENT_NAME = 'Content-Name' -K_CONTENT_LENGTH = 'Content-Length' -K_CONTENT_RANGE = 'Content-Range' -K_CONTENT_ENCODING = 'Content-Encoding' - -# BITS Protocol header values -V_ACK = 'Ack' - -class ThreadedHTTPServer(BaseHTTPServer.HTTPServer): - - def handle_error(self, request, client_address): - exctype, value = sys.exc_info()[:2] - self.logger.error('Error: %s', value) - -# BITS server errors -class BITSServerHResult(object): - # default context - BG_ERROR_CONTEXT_REMOTE_FILE = hex(0x5) - # official error codes - BG_E_TOO_LARGE = hex(0x80200020) - E_INVALIDARG = hex(0x80070057) - E_ACCESSDENIED = hex(0x80070005) - ZERO = hex(0x0) # protocol specification does not give a name for this HRESULT - # custom error code - ERROR_CODE_GENERIC = hex(0x1) - - -class HTTPStatus(object): - # Successful 2xx - OK = 200 - CREATED = 201 - # Client Error 4xx - BAD_REQUEST = 400 - FORBIDDEN = 403 - NOT_FOUND = 404 - CONFLICT = 409 - REQUESTED_RANGE_NOT_SATISFIABLE = 416 - # Server Error 5xx - INTERNAL_SERVER_ERROR = 500 - NOT_IMPLEMENTED = 501 - - -class BITSServerException(Exception): - pass - -class ClientProtocolNotSupported(BITSServerException): - def __init__(self, supported_protocols): - super(ClientProtocolNotSupported, self).__init__("Server supports neither of the requested protocol versions") - self.requested_protocols = str(supported_protocols) - - -class ServerInternalError(BITSServerException): - def __init__(self, internal_exception): - super(ServerInternalError, self).__init__("Internal server error encountered") - self.internal_exception = internal_exception - - -class InvalidFragment(BITSServerException): - def __init__(self, last_range_end, new_range_start): - super(ServerInternalError, self).__init__("Invalid fragment received on server") - self.last_range_end = last_range_end - self.new_range_start = new_range_start - - -class FragmentTooLarge(BITSServerException): - def __init__(self, fragment_size): - super(FragmentTooLarge, self).__init__("Oversized fragment received on server") - self.fragment_size = fragment_size - - -class UploadAccessDenied(BITSServerException): - def __init__(self): - super(UploadAccessDenied, self).__init__("Write access to requested file upload is denied") - - -class BITSUploadSession(object): - - # holds the file paths that has an active upload session - files_in_use = [] - - def __init__(self, absolute_file_path, fragment_size_limit): - self.fragment_size_limit = fragment_size_limit - self.absolute_file_path = absolute_file_path - self.fragments = [] - self.expected_file_length = -1 - - # case the file already exists - if os.path.exists(self.absolute_file_path): - # case the file is actually a directory - if os.path.isdir(self.absolute_file_path): - self._status_code = HTTPStatus.FORBIDDEN - # case the file is being uploaded in another active session - elif self.absolute_file_path in BITSUploadSession.files_in_use: - self._status_code = HTTPStatus.CONFLICT - # case file exists on server - we overwrite the file with the new upload - else: - BITSUploadSession.files_in_use.append(self.absolute_file_path) - self.__open_file() - # case file does not exist but its parent folder does exist - we create the file - elif os.path.exists(os.path.dirname(self.absolute_file_path)): - BITSUploadSession.files_in_use.append(self.absolute_file_path) - self.__open_file() - # case file does not exist nor its parent folder - we don't create the directory tree - else: - self._status_code = HTTPStatus.FORBIDDEN - - def __open_file(self): - try: - self.file = open(self.absolute_file_path, "wb") - self._status_code = HTTPStatus.OK - except Exception: - self._status_code = HTTPStatus.FORBIDDEN - - def __get_final_data_from_fragments(self): - """ - Combines all accepted fragments' data into one string - """ - return "".join([frg['data'] for frg in self.fragments]) - - def get_last_status_code(self): - return self._status_code - - def add_fragment(self, file_total_length, range_start, range_end, data): - """ - Applies new fragment received from client to the upload session. - Returns a boolean: is the new fragment last in session - """ - # check if fragment size exceeds server limit - if self.fragment_size_limit < range_end - range_start: - raise FragmentTooLarge(range_end - range_start) - - # case new fragment is the first fragment in this session - if self.expected_file_length == -1: - self.expected_file_length = file_total_length - - last_range_end = self.fragments[-1]['range_end'] if self.fragments else -1 - if last_range_end + 1 < range_start: - # case new fragment's range is not contiguous with the previous fragment - # will cause the server to respond with status code 416 - raise InvalidFragment(last_range_end, range_start) - elif last_range_end + 1 > range_start: - # case new fragment partially overlaps last fragment - # BITS protocol states that server should treat only the non-overlapping part - range_start = last_range_end + 1 - - self.fragments.append( - {'range_start': range_start, - 'range_end': range_end, - 'data': data}) - - # case new fragment is the first fragment in this session, - # we write the final uploaded data to file - if range_end + 1 == self.expected_file_length: - self.file.write(self.__get_final_data_from_fragments()) - return True - - return False - - def close(self): - self.file.flush() - self.file.close() - BITSUploadSession.files_in_use.remove(self.absolute_file_path) - - -class SimpleBITSRequestHandler(SimpleHTTPRequestHandler): - - protocol_version = "HTTP/1.1" - supported_protocols = ["{7df0354d-249b-430f-820d-3d2a9bef4931}"] # The only existing protocol version to date - fragment_size_limit = 100*1024*1024 # bytes - - def do_HEAD(self): - self.server.logger.info('Received HEAD request') - - # Process request - self.server.logger.info(self.requestline) - for line in str(self.headers).split("\n"): - self.server.logger.info(INDENT + line) - - # Prepare response - self.send_response(200) - self.send_header("Content-type", "text/html") - self.end_headers() - - def __send_response(self, headers_dict={}, status_code=HTTPStatus.OK, data=""): - """ - Sends server response w/ headers and status code - """ - self.send_response(status_code) - for k, v in headers_dict.iteritems(): - self.send_header(k, v) - self.end_headers() - - self.wfile.write(data) - - def __release_resources(self): - """ - Releases server resources for a session termination caused by either: - Close-Session or Cancel-Session - """ - headers = { - K_BITS_PACKET_TYPE: V_ACK, - K_CONTENT_LENGTH: '0' - } - - try: - session_id = self.headers.get(K_BITS_SESSION_ID, None).lower() - headers[K_BITS_SESSION_ID] = session_id - self.server.logger.info("Closing BITS-Session-Id: %s", session_id) - - self.sessions[session_id].close() - self.sessions.pop(session_id, None) - - status_code = HTTPStatus.OK - except AttributeError: - self.__send_response(headers, status_code = HTTPStatus.BAD_REQUEST) - return - except Exception as e: - raise ServerInternalError(e) - - self.__send_response(headers, status_code = status_code) - - def _handle_fragment(self): - """ - Handles a new Fragment packet from the client, adding it to the relevant upload session - """ - headers = { - K_BITS_PACKET_TYPE: V_ACK, - K_CONTENT_LENGTH: '0' - } - - try: - # obtain client headers - session_id = self.headers.get(K_BITS_SESSION_ID, None).lower() - content_length = int(self.headers.get(K_CONTENT_LENGTH, None)) - content_name = self.headers.get(K_CONTENT_NAME, None) - content_encoding = self.headers.get(K_CONTENT_ENCODING, None) - content_range = self.headers.get(K_CONTENT_RANGE, None).split(" ")[-1] - # set response headers's session id - headers[K_BITS_SESSION_ID] = session_id - # normalize fragment details - crange, total_length = content_range.split("/") - total_length = int(total_length) - range_start, range_end = [int(num) for num in crange.split("-")] - except AttributeError, IndexError: - self.__send_response(status_code = HTTPStatus.BAD_REQUEST) - return - - data = self.rfile.read(content_length) - - try: - is_last_fragment = self.sessions[session_id].add_fragment( - total_length, range_start, range_end, data) - headers['BITS-Received-Content-Range'] = range_end + 1 - except InvalidFragment as e: - headers[K_BITS_ERROR_CODE] = BITSServerHResult.ZERO - headers[K_BITS_ERROR_CONTEXT] = BITSServerHResult.BG_ERROR_CONTEXT_REMOTE_FILE - status_code = HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE - self.server.logger.error("ERROR processing new fragment (BITS-Session-Id: %s)." + \ - "New fragment range (%d) is not contiguous with last received (%d). context:%s, code:%s, exception:%s", - session_id, - e.new_range_start, - e.last_range_end, - headers[K_BITS_ERROR_CONTEXT], - headers[K_BITS_ERROR_CODE], - repr(e)) - except FragmentTooLarge as e: - headers[K_BITS_ERROR_CODE] = BITSServerHResult.BG_E_TOO_LARGE - headers[K_BITS_ERROR_CONTEXT] = BITSServerHResult.BG_ERROR_CONTEXT_REMOTE_FILE - status_code = HTTPStatus.INTERNAL_SERVER_ERROR - self.server.logger.error("ERROR processing new fragment (BITS-Session-Id: %s)." + \ - "New fragment size (%d) exceeds server limit (%d). context:%s, code:%s, exception:%s", - session_id, - e.fragment_size, - SimpleBITSRequestHandler.fragment_size_limit, - headers[K_BITS_ERROR_CONTEXT], - headers[K_BITS_ERROR_CODE], - repr(e)) - except Exception as e: - raise ServerInternalError(e) - - status_code = HTTPStatus.OK - self.__send_response(headers, status_code = status_code) - - def _handle_ping(self): - """ - Handles Ping packet from client - """ - self.server.logger.debug("%s RECEIVED", "PING") - headers = { - K_BITS_PACKET_TYPE: V_ACK, - K_BITS_ERROR_CODE : '1', - K_BITS_ERROR_CONTEXT: '', - K_CONTENT_LENGTH: '0' - } - self.__send_response(headers, status_code = HTTPStatus.OK) - - def __get_current_session_id(self): - return str(hash((self.connection.getpeername()[0], self.path))) - - def _handle_cancel_session(self): - self.server.logger.debug("%s RECEIVED", "CANCEL-SESSION") - return self.__release_resources() - - def _handle_close_session(self): - self.server.logger.debug("%s RECEIVED", "CLOSE-SESSION") - return self.__release_resources() - - - def _handle_create_session(self): - """ - Handles Create-Session packet from client. Creates the UploadSession. - The unique ID that identifies a session in this server is a hash of the client's address and requested path. - """ - self.server.logger.debug("%s RECEIVED", "CREATE-SESSION") - - headers = { - K_BITS_PACKET_TYPE: V_ACK, - K_CONTENT_LENGTH: '0' - } - - if not getattr(self, "sessions", False): - self.sessions = dict() - try: - # check if server's protocol version is supported in client - client_supported_protocols = \ - self.headers.get(K_BITS_SUPPORTED_PROTOCOLS, None).lower().split(" ") - protocols_intersection = set(client_supported_protocols).intersection( - SimpleBITSRequestHandler.supported_protocols) - - # case mutual supported protocol is found - if protocols_intersection: - headers[K_BITS_PROTOCOL] = list(protocols_intersection)[0] - - safe_path = self.server.bits_file_prefix + '_' + urllib.quote(self.path, '') - absolute_file_path = ListenerBase.safe_join(os.getcwd(), safe_path) - - session_id = self.__get_current_session_id() - self.server.logger.info("Creating BITS-Session-Id: %s", session_id) - if session_id not in self.sessions: - self.sessions[session_id] = BITSUploadSession(absolute_file_path, SimpleBITSRequestHandler.fragment_size_limit) - - headers[K_BITS_SESSION_ID] = session_id - status_code = self.sessions[session_id].get_last_status_code() - if status_code == HTTPStatus.FORBIDDEN: - raise UploadAccessDenied() - # case no mutual supported protocol is found - else: - raise ClientProtocolNotSupported(client_supported_protocols) - except AttributeError: - self.__send_response(headers, status_code = HTTPStatus.BAD_REQUEST) - return - except ClientProtocolNotSupported as e: - status_code = HTTPStatus.BAD_REQUEST - headers[K_BITS_ERROR_CODE] = BITSServerHResult.E_INVALIDARG - headers[K_BITS_ERROR_CONTEXT] = BITSServerHResult.BG_ERROR_CONTEXT_REMOTE_FILE - self.server.logger.error("ERROR creating new session - protocol mismatch (%s). context:%s, code:%s, exception:%s", - e.requested_protocols, - headers[K_BITS_ERROR_CONTEXT], - headers[K_BITS_ERROR_CODE], - repr(e)) - except UploadAccessDenied as e: - headers[K_BITS_ERROR_CODE] = BITSServerHResult.E_ACCESSDENIED - headers[K_BITS_ERROR_CONTEXT] = BITSServerHResult.BG_ERROR_CONTEXT_REMOTE_FILE - self.lserver.logger.error("ERROR creating new session - Access Denied. context:%s, code:%s, exception:%s", - headers[K_BITS_ERROR_CONTEXT], - headers[K_BITS_ERROR_CODE], - repr(e)) - except Exception as e: - raise ServerInternalError(e) - - - if status_code == HTTPStatus.OK or status_code == HTTPStatus.CREATED: - headers[K_ACCEPT_ENCODING] = 'identity' - - self.__send_response(headers, status_code = status_code) - - def do_BITS_POST(self): - headers = {} - bits_packet_type = self.headers.getheaders(K_BITS_PACKET_TYPE)[0].lower() - try: - do_function = getattr(self, "_handle_%s" % bits_packet_type.replace("-", "_")) - try: - do_function() - return - except ServerInternalError as e: - status_code = HTTPStatus.INTERNAL_SERVER_ERROR - headers[K_BITS_ERROR_CODE] = BITSServerHResult.ERROR_CODE_GENERIC - except AttributeError as e: - # case an Unknown BITS-Packet-Type value was received by the server - status_code = HTTPStatus.BAD_REQUEST - headers[K_BITS_ERROR_CODE] = BITSServerHResult.E_INVALIDARG - - headers[K_BITS_ERROR_CONTEXT] = BITSServerHResult.BG_ERROR_CONTEXT_REMOTE_FILE - self.server.logger.error("Internal BITS Server Error. context:%s, code:%s, exception:%s", - headers[K_BITS_ERROR_CONTEXT], - headers[K_BITS_ERROR_CODE], - repr(e.internal_exception)) - self.__send_response(headers, status_code = status_code) - -class BITSListener(object): - - def taste(self, data, dport): - request_methods = ['BITS_POST',] - - confidence = 1 if dport in [80, 443] else 0 - - for method in request_methods: - if data.lstrip().startswith(method): - confidence += 2 - continue - - return confidence - - def __init__( - self, - config={}, - name='BITSListener', - logging_level=logging.DEBUG, - running_listeners=None - ): - - self.logger = logging.getLogger(name) - self.logger.setLevel(logging_level) - - self.config = config - self.name = name - self.local_ip = config.get('ipaddr') - self.server = None - self.running_listeners = running_listeners - self.NAME = 'BITS' - self.PORT = self.config.get('port') - - self.logger.info('Starting...') - - self.logger.debug('Initialized with config:') - for key, value in config.iteritems(): - self.logger.debug(' %10s: %s', key, value) - - self.bits_file_prefix = self.config.get('bitsfileprefix', 'bits') - - def start(self): - self.logger.debug('Starting...') - self.server = ThreadedHTTPServer((self.local_ip, int(self.config.get('port'))), SimpleBITSRequestHandler) - self.server.logger = self.logger - self.server.bits_file_prefix = self.bits_file_prefix - self.server.config = self.config - - if self.config.get('usessl') == 'Yes': - self.logger.debug('Using SSL socket.') - - keyfile_path = ListenerBase.abs_config_path('privkey.pem') - if keyfile_path is None: - self.logger.error('Could not locate privkey.pem') - sys.exit(1) - - certfile_path = ListenerBase.abs_config_path('server.pem') - if certfile_path is None: - self.logger.error('Could not locate certfile.pem') - sys.exit(1) - - self.server.socket = ssl.wrap_socket(self.server.socket, keyfile=keyfile_path, certfile=certfile_path, server_side=True, ciphers='RSA') - - self.server_thread = threading.Thread(target=self.server.serve_forever) - self.server_thread.daemon = True - self.server_thread.start() - - def stop(self): - self.logger.info('Stopping...') - if self.server: - self.server.shutdown() - self.server.server_close() - -def test(config): - pass - -def main(): - """ - Run from the flare-fakenet-ng root dir with the following command: - - python2 -m self.BITSListener - - """ - logging.basicConfig(format='%(asctime)s [%(name)15s] %(message)s', datefmt='%m/%d/%y %I:%M:%S %p', level=logging.DEBUG) - - config = {'port': '80', 'usessl': 'No' } - - listener = BITSListener(config) - listener.start() - - ########################################################################### - # Run processing - import time - - try: - while True: - time.sleep(1) - except KeyboardInterrupt: - pass - - ########################################################################### - # Run tests - test(config) - -if __name__ == '__main__': - main() diff --git a/fakenet/listeners/BannerFactory.py b/fakenet/listeners/BannerFactory.py index c2de6b04..827b0bcf 100644 --- a/fakenet/listeners/BannerFactory.py +++ b/fakenet/listeners/BannerFactory.py @@ -1,3 +1,5 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import random import socket import string @@ -140,7 +142,7 @@ def genBanner(self, config, bannerdict, defaultbannerkey='!generic'): if banner.startswith('!'): banner = banner[1:] if banner.lower() == 'random': - banner = random.choice(bannerdict.keys()) + banner = random.choice(list(bannerdict.keys())) elif banner not in bannerdict: raise ValueError( 'Banner config escape !%s not a valid banner key' % diff --git a/fakenet/listeners/DNSListener.py b/fakenet/listeners/DNSListener.py index ef401d1b..da410b81 100644 --- a/fakenet/listeners/DNSListener.py +++ b/fakenet/listeners/DNSListener.py @@ -1,8 +1,10 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import logging import threading import netifaces -import SocketServer +import socketserver from dnslib import * import ssl @@ -45,7 +47,7 @@ def __init__( self.logger.debug('Starting...') self.logger.debug('Initialized with config:') - for key, value in config.iteritems(): + for key, value in config.items(): self.logger.debug(' %10s: %s', key, value) def start(self): @@ -84,7 +86,7 @@ def parse(self,data): # Parse data as DNS d = DNSRecord.parse(data) - except Exception, e: + except Exception as e: self.server.logger.error('Error: Invalid DNS Request') for line in hexdump_table(data): self.server.logger.warning(INDENT + line) @@ -190,7 +192,7 @@ def parse(self,data): return response -class UDPHandler(DNSHandler, SocketServer.BaseRequestHandler): +class UDPHandler(DNSHandler, socketserver.BaseRequestHandler): def handle(self): @@ -204,10 +206,10 @@ def handle(self): except socket.error as msg: self.server.logger.error('Error: %s', msg.strerror or msg) - except Exception, e: + except Exception as e: self.server.logger.error('Error: %s', e) -class TCPHandler(DNSHandler, SocketServer.BaseRequestHandler): +class TCPHandler(DNSHandler, socketserver.BaseRequestHandler): def handle(self): @@ -234,18 +236,18 @@ def handle(self): except socket.error as msg: self.server.logger.error('Error: %s', msg.strerror) - except Exception, e: + except Exception as e: self.server.logger.error('Error: %s', e) -class ThreadedUDPServer(SocketServer.ThreadingMixIn, SocketServer.UDPServer): +class ThreadedUDPServer(socketserver.ThreadingMixIn, socketserver.UDPServer): # Override SocketServer.UDPServer to add extra parameters def __init__(self, server_address, config, logger, RequestHandlerClass): self.config = config self.logger = logger - SocketServer.UDPServer.__init__(self, server_address, RequestHandlerClass) + socketserver.UDPServer.__init__(self, server_address, RequestHandlerClass) -class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): +class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): # Override default value allow_reuse_address = True @@ -254,7 +256,7 @@ class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): def __init__(self, server_address, config, logger, RequestHandlerClass): self.config = config self.logger = logger - SocketServer.TCPServer.__init__(self,server_address,RequestHandlerClass) + socketserver.TCPServer.__init__(self,server_address,RequestHandlerClass) def hexdump_table(data, length=16): @@ -270,31 +272,31 @@ def hexdump_table(data, length=16): # Testing code def test(config): - print "\t[DNSListener] Testing 'google.com' A record." + print("\t[DNSListener] Testing 'google.com' A record.") query = DNSRecord(q=DNSQuestion('google.com',getattr(QTYPE,'A'))) answer_pkt = query.send('localhost', int(config.get('port', 53))) answer = DNSRecord.parse(answer_pkt) - print '-'*80 - print answer - print '-'*80 + print('-'*80) + print(answer) + print('-'*80) - print "\t[DNSListener] Testing 'google.com' MX record." + print("\t[DNSListener] Testing 'google.com' MX record.") query = DNSRecord(q=DNSQuestion('google.com',getattr(QTYPE,'MX'))) answer_pkt = query.send('localhost', int(config.get('port', 53))) answer = DNSRecord.parse(answer_pkt) - print '-'*80 - print answer + print('-'*80) + print(answer) - print "\t[DNSListener] Testing 'google.com' TXT record." + print("\t[DNSListener] Testing 'google.com' TXT record.") query = DNSRecord(q=DNSQuestion('google.com',getattr(QTYPE,'TXT'))) answer_pkt = query.send('localhost', int(config.get('port', 53))) answer = DNSRecord.parse(answer_pkt) - print '-'*80 - print answer - print '-'*80 + print('-'*80) + print(answer) + print('-'*80) def main(): logging.basicConfig(format='%(asctime)s [%(name)15s] %(message)s', datefmt='%m/%d/%y %I:%M:%S %p', level=logging.DEBUG) diff --git a/fakenet/listeners/FTPListener.py b/fakenet/listeners/FTPListener.py index 7945e758..71f08f9a 100644 --- a/fakenet/listeners/FTPListener.py +++ b/fakenet/listeners/FTPListener.py @@ -1,10 +1,12 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import logging import os import sys import threading -import SocketServer +import socketserver import ssl import socket @@ -16,21 +18,21 @@ from pyftpdlib.filesystems import AbstractedFS from pyftpdlib.servers import ThreadedFTPServer -import BannerFactory +from . import BannerFactory FAKEUSER = 'FAKEUSER' FAKEPWD = 'FAKEPWD' EXT_FILE_RESPONSE = { - '.html': u'FakeNet.html', - '.png' : u'FakeNet.png', - '.ico' : u'FakeNet.ico', - '.jpeg': u'FakeNet.jpg', - '.exe' : u'FakeNetMini.exe', - '.pdf' : u'FakeNet.pdf', - '.xml' : u'FakeNet.html', - '.txt' : u'FakeNet.txt', + '.html': 'FakeNet.html', + '.png' : 'FakeNet.png', + '.ico' : 'FakeNet.ico', + '.jpeg': 'FakeNet.jpg', + '.exe' : 'FakeNetMini.exe', + '.pdf' : 'FakeNet.pdf', + '.xml' : 'FakeNet.html', + '.txt' : 'FakeNet.txt', } # Adapted from various sources including https://github.com/turbo/openftp4 @@ -178,7 +180,7 @@ def open(self, filename, mode): file_basename, file_extension = os.path.splitext(filename) # Calculate absolute path to a fake file - filename = os.path.join(os.path.dirname(filename), EXT_FILE_RESPONSE.get(file_extension.lower(), u'FakeNetMini.exe')) + filename = os.path.join(os.path.dirname(filename), EXT_FILE_RESPONSE.get(file_extension.lower(), 'FakeNetMini.exe')) return super(FakeFS, self).open(filename, mode) @@ -186,7 +188,7 @@ def chdir(self, path): # If virtual directory does not exist change to the current directory if not self.lexists(path): - path = u'.' + path = '.' return super(FakeFS, self).chdir(path) @@ -207,12 +209,12 @@ def taste(self, data, dport): # See RFC5797 for full command list. Many of these commands are not likely # to be used but are included in case malware uses FTP in unexpected ways base_ftp_commands = [ - 'abor', 'acct', 'allo', 'appe', 'cwd', 'dele', 'help', 'list', 'mode', - 'nlst', 'noop', 'pass', 'pasv', 'port', 'quit', 'rein', 'rest', 'retr', - 'rnfr', 'rnto', 'site', 'stat', 'stor', 'stru', 'type', 'user' + b'abor', b'acct', b'allo', b'appe', b'cwd', b'dele', b'help', b'list', b'mode', + b'nlst', b'noop', b'pass', b'pasv', b'port', b'quit', b'rein', b'rest', b'retr', + b'rnfr', b'rnto', b'site', b'stat', b'stor', b'stru', b'type', b'user' ] opt_ftp_commands = [ - 'cdup', 'mkd', 'pwd', 'rmd', 'smnt', 'stou', 'syst' + b'cdup', b'mkd', b'pwd', b'rmd', b'smnt', b'stou', b'syst' ] confidence = 1 if dport == 21 else 0 @@ -247,7 +249,7 @@ def __init__(self, self.logger.debug('Starting...') self.logger.debug('Initialized with config:') - for key, value in config.iteritems(): + for key, value in config.items(): self.logger.debug(' %10s: %s', key, value) # Initialize ftproot directory @@ -263,8 +265,8 @@ def expand_ports(self, ports_list): if '-' not in i: ports.append(int(i)) else: - l,h = map(int, i.split('-')) - ports+= range(l,h+1) + l,h = list(map(int, i.split('-'))) + ports+= list(range(l,h+1)) return ports def start(self): diff --git a/fakenet/listeners/HTTPListener.py b/fakenet/listeners/HTTPListener.py index 806221a3..54821b2b 100644 --- a/fakenet/listeners/HTTPListener.py +++ b/fakenet/listeners/HTTPListener.py @@ -1,13 +1,15 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import logging -from ConfigParser import ConfigParser +from configparser import ConfigParser import os import sys import imp import threading -import SocketServer -import BaseHTTPServer +import socketserver +import http.server import ssl import socket @@ -120,7 +122,7 @@ def checkMatch(self, host, uri): def respond(self, req, meth, postdata=None): current_time = req.date_time_string() if self.raw_file: - up_to_date = self.raw_file.replace('', current_time) + up_to_date = self.raw_file.replace(b'', current_time.encode("utf-8")) req.wfile.write(up_to_date) elif self.handler: self.handler(req, meth, postdata) @@ -131,15 +133,15 @@ def respond(self, req, meth, postdata=None): if self.content_type: req.send_header('Content-Type', self.content_type) req.end_headers() - req.wfile.write(up_to_date) + req.wfile.write(up_to_date.encode("utf-8")) class HTTPListener(object): def taste(self, data, dport): - request_methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', - 'OPTIONS', 'CONNECT', 'PATCH'] + request_methods = [b'GET', b'HEAD', b'POST', b'PUT', b'DELETE', b'TRACE', + b'OPTIONS', b'CONNECT', b'PATCH'] confidence = 1 if dport in [80, 443] else 0 @@ -173,7 +175,7 @@ def __init__( self.port = self.config.get('port', 80) self.logger.debug('Initialized with config:') - for key, value in config.iteritems(): + for key, value in config.items(): self.logger.debug(' %10s: %s', key, value) # Initialize webroot directory @@ -245,16 +247,16 @@ def stop(self): self.server.server_close() -class ThreadedHTTPServer(BaseHTTPServer.HTTPServer): +class ThreadedHTTPServer(http.server.HTTPServer): def handle_error(self, request, client_address): exctype, value = sys.exc_info()[:2] self.logger.error('Error: %s', value) -class ThreadedHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): +class ThreadedHTTPRequestHandler(http.server.BaseHTTPRequestHandler): def __init__(self, *args): - BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args) + http.server.BaseHTTPRequestHandler.__init__(self, *args) self.logger = self.server.logger def version_string(self): @@ -262,7 +264,7 @@ def version_string(self): def setup(self): self.request.settimeout(int(self.server.config.get('timeout', 10))) - BaseHTTPServer.BaseHTTPRequestHandler.setup(self) + http.server.BaseHTTPRequestHandler.setup(self) def doCustomResponse(self, meth, post_data=None): uri = self.path @@ -308,7 +310,7 @@ def do_GET(self): self.wfile.write(response) def do_POST(self): - post_body = '' + post_body = b'' content_len = int(self.headers.get('content-length', 0)) post_body = self.rfile.read(content_len) @@ -317,8 +319,8 @@ def do_POST(self): self.server.logger.info(INDENT + self.requestline) for line in str(self.headers).split("\n"): self.server.logger.info(INDENT + line) - for line in post_body.split("\n"): - self.server.logger.info(INDENT + line) + for line in post_body.split(b"\n"): + self.server.logger.info(INDENT.encode('utf-8') + line) # Store HTTP Posts if self.server.config.get('dumphttpposts') and self.server.config['dumphttpposts'].lower() == 'yes': @@ -328,8 +330,8 @@ def do_POST(self): http_f = open(http_filename, 'wb') if http_f: - http_f.write(self.requestline + "\r\n") - http_f.write(str(self.headers) + "\r\n") + http_f.write(self.requestline.encode('utf-8') + b"\r\n") + http_f.write(str(self.headers).encode('utf-8') + b"\r\n") http_f.write(post_body) http_f.close() @@ -382,7 +384,7 @@ def get_response(self, path): try: f = open(response_filename, 'rb') - except Exception, e: + except Exception as e: self.server.logger.error('Failed to open response file: %s', response_filename) response_type = 'text/html' else: @@ -403,20 +405,20 @@ def test(config): url = "%s://localhost:%s" % ('http' if config.get('usessl') == 'No' else 'https', int(config.get('port', 8080))) - print "\t[HTTPListener] Testing HEAD request." - print '-'*80 - print requests.head(url, verify=False, stream=True).text - print '-'*80 + print("\t[HTTPListener] Testing HEAD request.") + print('-'*80) + print(requests.head(url, verify=False, stream=True).text) + print('-'*80) - print "\t[HTTPListener] Testing GET request." - print '-'*80 - print requests.get(url, verify=False, stream=True).text - print '-'*80 + print("\t[HTTPListener] Testing GET request.") + print('-'*80) + print(requests.get(url, verify=False, stream=True).text) + print('-'*80) - print "\t[HTTPListener] Testing POST request." - print '-'*80 - print requests.post(url, {'param1':'A'*80, 'param2':'B'*80}, verify=False, stream=True).text - print '-'*80 + print("\t[HTTPListener] Testing POST request.") + print('-'*80) + print(requests.post(url, {'param1':'A'*80, 'param2':'B'*80}, verify=False, stream=True).text) + print('-'*80) def main(): """ diff --git a/fakenet/listeners/IRCListener.py b/fakenet/listeners/IRCListener.py index 9b38b5cf..f2dcb98b 100644 --- a/fakenet/listeners/IRCListener.py +++ b/fakenet/listeners/IRCListener.py @@ -1,15 +1,17 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import logging import sys import os import threading -import SocketServer +import socketserver import ssl import socket -import BannerFactory +from . import BannerFactory from . import * @@ -48,15 +50,15 @@ def taste(self, data, dport): ] # ubuntu xchat uses 8001 - ports = [194, 6667, range(6660, 7001), 8001] + ports = [194, 6667, list(range(6660, 7001)), 8001] confidence = 1 if dport in ports else 0 - + data = data.lstrip() # remove optional prefix - if data.startswith(':'): - data = data.split(' ')[0] + if data.startswith(b':'): + data = data.split(b' ')[0].decode() for command in commands: if data.startswith(command): @@ -85,7 +87,7 @@ def __init__(self, self.logger.debug('Starting...') self.logger.debug('Initialized with config:') - for key, value in config.iteritems(): + for key, value in config.items(): self.logger.debug(' %10s: %s', key, value) def start(self): @@ -112,7 +114,7 @@ def genBanner(self): bannerfactory = BannerFactory.BannerFactory() return bannerfactory.genBanner(self.config, BANNERS) -class ThreadedTCPRequestHandler(SocketServer.BaseRequestHandler): +class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): def handle(self): @@ -125,14 +127,14 @@ def handle(self): while True: - data = self.request.recv(1024) + data = self.request.recv(1024).decode() if not data: break elif len(data) > 0: - for line in data.split("\n"): + for line in data.split('\n'): if line and len(line) > 0: @@ -150,7 +152,7 @@ def handle(self): except socket.error as msg: self.server.logger.error('Error: %s', msg.strerror or msg) - except Exception, e: + except Exception as e: self.server.logger.error('Error: %s', e) def irc_DEFAULT(self, cmd, params): @@ -173,10 +175,10 @@ def irc_USER(self, cmd, params): self.user = user self.mode = mode self.realname = realname - self.request.sendall('') + self.request.sendall(b'') def irc_PING(self, cmd, params): - self.request.sendall(":%s PONG :%s" % (self.server.servername, self.server.servername)) + self.request.sendall((":%s PONG :%s" % (self.server.servername, self.server.servername)).encode()) def irc_JOIN(self, cmd, params): @@ -196,7 +198,7 @@ def irc_JOIN(self, cmd, params): self.server.logger.info('Client %s is joining channel %s with no key', self.nick, channel_name) - self.request.sendall(":root TOPIC %s :FakeNet\r\n" % channel_name) + self.request.sendall((":root TOPIC %s :FakeNet\r\n" % channel_name).encode()) self.irc_send_client("JOIN :%s" % channel_name) nicks = ['botmaster', 'bot', 'admin', 'root', 'master'] @@ -230,15 +232,15 @@ def irc_PART(self, cmd, params): pass def irc_send_server(self, code, message): - self.request.sendall(":%s %s %s\r\n" % (self.server.servername, code, message)) + self.request.sendall((":%s %s %s\r\n" % (self.server.servername, code, message)).encode()) def irc_send_client(self, message): self.irc_send_client_custom(self.nick, self.user, self.server.servername, message) def irc_send_client_custom(self, nick, user, servername, message): - self.request.sendall(":%s!%s@%s %s\r\n" % (nick, user, servername, message)) + self.request.sendall((":%s!%s@%s %s\r\n" % (nick, user, servername, message)).encode()) -class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): +class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): # Avoid [Errno 98] Address already in use due to TIME_WAIT status on TCP # sockets, for details see: # https://stackoverflow.com/questions/4465959/python-errno-98-address-already-in-use diff --git a/fakenet/listeners/ListenerBase.py b/fakenet/listeners/ListenerBase.py index d3156073..19dd22ef 100644 --- a/fakenet/listeners/ListenerBase.py +++ b/fakenet/listeners/ListenerBase.py @@ -1,5 +1,8 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import logging import os +import sys def safe_join(root, path): """ @@ -30,8 +33,12 @@ def abs_config_path(path): if os.path.exists(abspath): return abspath - # Try to locate the location relative to application path - relpath = os.path.join(os.path.dirname(os.path.dirname(__file__)), path) + if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): + relpath = os.path.join(os.getcwd(), path) + else: + + # Try to locate the location relative to application path + relpath = os.path.join(os.path.dirname(os.path.dirname(__file__)), path) if os.path.exists(relpath): return os.path.abspath(relpath) diff --git a/fakenet/listeners/POPListener.py b/fakenet/listeners/POPListener.py index 87ffa104..fc04ca36 100644 --- a/fakenet/listeners/POPListener.py +++ b/fakenet/listeners/POPListener.py @@ -1,17 +1,19 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import logging import sys import os import threading -import SocketServer +import socketserver import ssl import socket from . import * -EMAIL = """From: "Bob Example" +EMAIL = b"""From: "Bob Example" To: Alice Example Cc: theboss@example.com Date: Tue, 15 January 2008 16:02:43 -0500 @@ -30,8 +32,8 @@ class POPListener(object): # POP is the protocol until the client sends a message. def taste(self, data, dport): - commands = [ 'QUIT', 'STAT', 'LIST', 'RETR', 'DELE', 'NOOP', 'RSET', - 'TOP', 'UIDL', 'USER', 'PASS', 'APOP' ] + commands = [ b'QUIT', b'STAT', b'LIST', b'RETR', b'DELE', b'NOOP', b'RSET', + b'TOP', b'UIDL', b'USER', b'PASS', b'APOP' ] confidence = 1 if dport == 110 else 0 @@ -61,7 +63,7 @@ def __init__(self, self.logger.debug('Starting...') self.logger.debug('Initialized with config:') - for key, value in config.iteritems(): + for key, value in config.items(): self.logger.debug(' %10s: %s', key, value) def start(self): @@ -99,7 +101,7 @@ def stop(self): self.server.shutdown() self.server.server_close() -class ThreadedTCPRequestHandler(SocketServer.BaseRequestHandler): +class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): def handle(self): @@ -108,7 +110,7 @@ def handle(self): try: - self.request.sendall("+OK FakeNet POP3 Server Ready\r\n") + self.request.sendall(b"+OK FakeNet POP3 Server Ready\r\n") while True: data = self.request.recv(1024) @@ -118,125 +120,125 @@ def handle(self): elif len(data) > 0: - for line in data.split("\r\n"): + for line in data.split(b"\r\n"): if line and len(line) > 0: - if ' ' in line: - cmd, params = line.split(' ', 1) + if b' ' in line: + cmd, params = line.split(b' ', 1) else: - cmd, params = line, '' + cmd, params = line, b'' - handler = getattr(self, 'pop_%s' % (cmd.upper()), self.pop_DEFAULT) + handler = getattr(self, 'pop_%s' % (cmd.decode("utf-8").upper()), self.pop_DEFAULT) handler(cmd, params) except socket.timeout: self.server.logger.warning('Connection timeout') except socket.error as msg: - self.server.logger.error('Error: %s', msg.strerror or msg) + self.server.logger.error('Socket Error: %s', msg.strerror or msg) - except Exception, e: + except Exception as e: self.server.logger.error('Error: %s', e) def pop_DEFAULT(self, cmd, params): self.server.logger.info('Client issued an unknown command %s %s', cmd, params) - self.request.sendall("-ERR Unknown command\r\n") + self.request.sendall(b"-ERR Unknown command\r\n") def pop_APOP(self, cmd, params): - if ' ' in params: - mailbox_name, digest = params.split(' ', 1) + if b' ' in params: + mailbox_name, digest = params.split(b' ', 1) self.server.logger.info('Client requests access to mailbox %s', mailbox_name) - self.request.sendall("+OK %s's maildrop has 2 messages (320 octets)\r\n" % mailbox_name) + self.request.sendall(b"+OK %s's maildrop has 2 messages (320 octets)\r\n" % mailbox_name) else: self.server.logger.info('Client sent invalid APOP command: APOP %s', params) - self.request.sendall("-ERR\r\n") + self.request.sendall(b"-ERR\r\n") def pop_RPOP(self, cmd, params): mailbox_name = params self.server.logger.info('Client requests access to mailbox %s', mailbox_name) - self.request.sendall("+OK %s's maildrop has 2 messages (320 octets)\r\n" % mailbox_name) + self.request.sendall(b"+OK %s's maildrop has 2 messages (320 octets)\r\n" % mailbox_name) def pop_USER(self, cmd, params): self.server.logger.info('Client user: %s', params) - self.request.sendall("+OK User accepted\r\n") + self.request.sendall(b"+OK User accepted\r\n") def pop_PASS(self, cmd, params): self.server.logger.info('Client password: %s', params) - self.request.sendall("+OK Pass accepted\r\n") + self.request.sendall(b"+OK Pass accepted\r\n") def pop_STAT(self, cmd, params): - self.request.sendall("+OK 2 320\r\n") + self.request.sendall(b"+OK 2 320\r\n") def pop_LIST(self, cmd, params): # List all messages - if params == '': + if params == b'': - self.request.sendall("+OK 2 messages (320 octets)\r\n") - self.request.sendall("1 120\r\n") - self.request.sendall("2 200\r\n") - self.request.sendall(".\r\n") + self.request.sendall(b"+OK 2 messages (320 octets)\r\n") + self.request.sendall(b"1 120\r\n") + self.request.sendall(b"2 200\r\n") + self.request.sendall(b".\r\n") # List individual message else: - self.request.sendall("+OK %d 200\r\n" % params) - self.request.sendall(".\r\n") + self.request.sendall(b"+OK %d 200\r\n" % params) + self.request.sendall(b".\r\n") def pop_RETR(self, cmd, params): self.server.logger.info('Client requests message %s', params) - self.request.sendall("+OK 120 octets\r\n") - self.request.sendall(EMAIL + "\r\n") - self.request.sendall(".\r\n") + self.request.sendall(b"+OK 120 octets\r\n") + self.request.sendall(EMAIL + b"\r\n") + self.request.sendall(b".\r\n") def pop_DELE(self, cmd, params): self.server.logger.info('Client requests message %s to be deleted', params) - self.request.sendall("+OK message %s deleted\r\n", params) + self.request.sendall(b"+OK message %s deleted\r\n", params) def pop_NOOP(self, cmd, params): - self.request.sendall("+OK\r\n") + self.request.sendall(b"+OK\r\n") def pop_RSET(self, cmd, params): - self.request.sendall("+OK maildrop has 2 messages (320 octets)\r\n") + self.request.sendall(b"+OK maildrop has 2 messages (320 octets)\r\n") def pop_TOP(self, cmd, params): - self.request.sendall("+OK\r\n") - self.request.sendall("1 120\r\n") - self.request.sendall("2 200\r\n") - self.request.sendall(".\r\n") + self.request.sendall(b"+OK\r\n") + self.request.sendall(b"1 120\r\n") + self.request.sendall(b"2 200\r\n") + self.request.sendall(b".\r\n") def pop_UIDL(self, cmd, params): - if params == '': - self.request.sendall("+OK\r\n") - self.request.sendall("1 whqtswO00WBw418f9t5JxYwZa\r\n") - self.request.sendall("2 QhdPYR:00WBw1Ph7x7a\r\n") - self.request.sendall(".\r\n") + if params == b'': + self.request.sendall(b"+OK\r\n") + self.request.sendall(b"1 whqtswO00WBw418f9t5JxYwZa\r\n") + self.request.sendall(b"2 QhdPYR:00WBw1Ph7x7a\r\n") + self.request.sendall(b".\r\n") else: - self.request.sendall("+OK %s QhdPYR:00WBw1Ph7x7\r\n", params) + self.request.sendall(b"+OK %s QhdPYR:00WBw1Ph7x7\r\n", params) def pop_QUIT(self, cmd, params): - self.request.sendall("+OK FakeNet POP3 server signing off\r\n") + self.request.sendall(b"+OK FakeNet POP3 server signing off\r\n") -class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): +class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): pass ############################################################################### @@ -254,8 +256,8 @@ def test(config): server.pass_('password') logger.info('Listing and retrieving messages.') - print server.list() - print server.retr(1) + print(server.list()) + print(server.retr(1)) server.quit() def main(): diff --git a/fakenet/listeners/ProxyListener.py b/fakenet/listeners/ProxyListener.py index f11f2564..9605d708 100644 --- a/fakenet/listeners/ProxyListener.py +++ b/fakenet/listeners/ProxyListener.py @@ -1,16 +1,18 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import socket -import SocketServer +import socketserver import threading import sys import glob import time import importlib -import Queue +import queue import select import logging import ssl from OpenSSL import SSL -from ssl_utils import ssl_detector +from .ssl_utils import ssl_detector from . import * import os @@ -38,7 +40,7 @@ def __init__( self.logger.debug('Starting...') self.logger.debug('Initialized with config:') - for key, value in config.iteritems(): + for key, value in config.items(): self.logger.debug(' %10s: %s', key, value) def start(self): @@ -130,10 +132,10 @@ def run(self): except Exception as e: self.logger.debug('Listener socket exception %s' % e.message) -class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): +class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): daemon_threads = True -class ThreadedUDPServer(SocketServer.ThreadingMixIn, SocketServer.UDPServer): +class ThreadedUDPServer(socketserver.ThreadingMixIn, socketserver.UDPServer): daemon_threads = True def get_top_listener(config, data, listeners, diverter, orig_src_ip, @@ -157,16 +159,16 @@ def get_top_listener(config, data, listeners, diverter, orig_src_ip, return top_listener -class ThreadedTCPRequestHandler(SocketServer.BaseRequestHandler): +class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): def handle(self): remote_sock = self.request # queue for data received from the listener - listener_q = Queue.Queue() + listener_q = queue.Queue() # queue for data received from remote - remote_q = Queue.Queue() + remote_q = queue.Queue() data = None ssl_remote_sock = None @@ -258,7 +260,7 @@ def handle(self): else: remote_sock.send(data) -class ThreadedUDPRequestHandler(SocketServer.BaseRequestHandler): +class ThreadedUDPRequestHandler(socketserver.BaseRequestHandler): def handle(self): @@ -301,8 +303,8 @@ def hexdump_table(data, length=16): hexdump_lines = [] for i in range(0, len(data), 16): chunk = data[i:i+16] - hex_line = ' '.join(["%02X" % ord(b) for b in chunk ] ) - ascii_line = ''.join([b if ord(b) > 31 and ord(b) < 127 else '.' for b in chunk ] ) + hex_line = ' '.join(["%02X" % b for b in chunk ] ) + ascii_line = ''.join([chr(b) if b > 31 and b < 127 else '.' for b in chunk ] ) hexdump_lines.append("%04X: %-*s %s" % (i, length*3, hex_line, ascii_line )) return hexdump_lines diff --git a/fakenet/listeners/RawListener.py b/fakenet/listeners/RawListener.py index 206f7e4a..a5ce8c87 100644 --- a/fakenet/listeners/RawListener.py +++ b/fakenet/listeners/RawListener.py @@ -1,5 +1,7 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import logging -from ConfigParser import ConfigParser +from configparser import ConfigParser import os import sys @@ -7,7 +9,7 @@ import base64 import threading -import SocketServer +import socketserver import ssl import socket @@ -55,7 +57,7 @@ def __init__(self, proto, name, conf, configroot): self.static = conf.get(spec_str) if self.static is not None: - self.static = self.static.rstrip('\r\n') + self.static = self.static.rstrip('\r\n').encode("utf-8") if not self.static is not None: b64_text = conf.get(spec_b64) @@ -107,7 +109,7 @@ def __init__(self, self.logger.debug('Starting...') self.logger.debug('Initialized with config:') - for key, value in config.iteritems(): + for key, value in config.items(): self.logger.debug(' %10s: %s', key, value) def start(self): @@ -193,27 +195,38 @@ def stop(self): self.server.shutdown() self.server.server_close() -class ThreadedTCPRequestHandler(SocketServer.BaseRequestHandler): +class SocketWithHexdumpRecv(): + def __init__(self, s, logger): + self.s = s + self.logger = logger - def handle(self): - # Hook to ensure that all `recv` calls transparently emit a hex dump - # in the log output, even if they occur within a user-implemented - # custom handler - def do_hexdump(data): - for line in hexdump_table(data): - self.server.logger.info(INDENT + line) + def __getattr__(self, item): + if 'recv' == item: + return self.recv + else: + return getattr(self.s, item) - orig_recv = self.request.recv + def do_hexdump(self, data): + for line in hexdump_table(data): + self.logger.info(INDENT + line) - def hook_recv(self, bufsize, flags=0): - data = orig_recv(bufsize, flags) - if data: - do_hexdump(data) - return data + # Hook to ensure that all `recv` calls transparently emit a hex dump + # in the log output, even if they occur within a user-implemented + # custom handler + def recv(self, n, flags=0): + data = self.s.recv(n, flags) + if data: + self.do_hexdump(data) + return data + +class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): - bound_meth = hook_recv.__get__(self.request, self.request.__class__) - setattr(self.request, 'recv', bound_meth) + def handle(self): + # Using a custom class, SocketWithHexdumpRecv, so as to hook the recv function + # setattr(self.request, 'recv', hook_recv) stopped working in python 3 + # as recv attribute became read-only + self.request = SocketWithHexdumpRecv(self.request, self.server.logger) # Timeout connection to prevent hanging self.request.settimeout(int(self.server.config.get('timeout', 5))) @@ -242,10 +255,10 @@ def hook_recv(self, bufsize, flags=0): except socket.error as msg: self.server.logger.error('Error: %s', msg.strerror or msg) - except Exception, e: + except Exception as e: self.server.logger.error('Error: %s', e) -class ThreadedUDPRequestHandler(SocketServer.BaseRequestHandler): +class ThreadedUDPRequestHandler(socketserver.BaseRequestHandler): def handle(self): (data,sock) = self.request @@ -264,16 +277,16 @@ def handle(self): except socket.error as msg: self.server.logger.error('Error: %s', msg.strerror or msg) - except Exception, e: + except Exception as e: self.server.logger.error('Error: %s', e) -class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): +class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): # Avoid [Errno 98] Address already in use due to TIME_WAIT status on TCP # sockets, for details see: # https://stackoverflow.com/questions/4465959/python-errno-98-address-already-in-use allow_reuse_address = True -class ThreadedUDPServer(SocketServer.ThreadingMixIn, SocketServer.UDPServer): +class ThreadedUDPServer(socketserver.ThreadingMixIn, socketserver.UDPServer): pass def hexdump_table(data, length=16): @@ -281,8 +294,8 @@ def hexdump_table(data, length=16): hexdump_lines = [] for i in range(0, len(data), 16): chunk = data[i:i+16] - hex_line = ' '.join(["%02X" % ord(b) for b in chunk ] ) - ascii_line = ''.join([b if ord(b) > 31 and ord(b) < 127 else '.' for b in chunk ] ) + hex_line = ' '.join(["%02X" % b for b in chunk ] ) + ascii_line = ''.join([chr(b) if b > 31 and b < 127 else '.' for b in chunk ] ) hexdump_lines.append("%04X: %-*s %s" % (i, length*3, hex_line, ascii_line )) return hexdump_lines @@ -292,11 +305,11 @@ def test(config): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - print "\t[RawListener] Sending request:\n%s" % "HELO\n" + print("\t[RawListener] Sending request:\n%s" % "HELO\n") try: # Connect to server and send data sock.connect(('localhost', int(config.get('port', 23)))) - sock.sendall("HELO\n") + sock.sendall(b"HELO\n") # Receive data from the server and shut down received = sock.recv(1024) diff --git a/fakenet/listeners/SMTPListener.py b/fakenet/listeners/SMTPListener.py index a01a2b89..c46ea0e1 100644 --- a/fakenet/listeners/SMTPListener.py +++ b/fakenet/listeners/SMTPListener.py @@ -1,10 +1,12 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import logging import sys import os import threading -import SocketServer +import socketserver import ssl import socket @@ -19,10 +21,10 @@ def taste(self, data, dport): # the conversation with '220' message. However, if the client connects # to a nonstandard port there is no way for the proxy to know that # SMTP is the protocol until the client sends a message. - commands = ['HELO', 'EHLO', 'MAIL FROM', 'RCPT TO', 'TURN', 'ATRN', - 'SIZE', 'ETRN', 'PIPELINING', 'CHUNKING', 'DATA', 'DSN', - 'RSET', 'VRFY', 'HELP', 'QUIT', 'X-EXPS GSSAPI', - 'X-EXPS=LOGIN', 'X-EXCH50', 'X-LINK2STATE'] + commands = [b'HELO', b'EHLO', b'MAIL FROM', b'RCPT TO', b'TURN', b'ATRN', + b'SIZE', b'ETRN', b'PIPELINING', b'CHUNKING', b'DATA', b'DSN', + b'RSET', b'VRFY', b'HELP', b'QUIT', b'X-EXPS GSSAPI', + b'X-EXPS=LOGIN', b'X-EXCH50', b'X-LINK2STATE'] ports = [25, 587, 465] confidence = 1 if dport in ports else 0 @@ -53,7 +55,7 @@ def __init__( self.logger.debug('Starting...') self.logger.debug('Initialized with config:') - for key, value in config.iteritems(): + for key, value in config.items(): self.logger.debug(' %10s: %s', key, value) def start(self): @@ -90,7 +92,7 @@ def stop(self): self.server.shutdown() self.server.server_close() -class ThreadedTCPRequestHandler(SocketServer.BaseRequestHandler): +class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): def handle(self): @@ -99,30 +101,30 @@ def handle(self): try: - self.request.sendall("%s\r\n" % self.server.config.get('banner',"220 FakeNet SMTP Service Ready")) + self.request.sendall(b"%s\r\n" % self.server.config.get('banner', "220 FakeNet SMTP Service Ready").encode()) while True: data = self.request.recv(4096) - for line in data.split("\n"): + for line in data.split(b"\n"): self.server.logger.debug(line) - command = data[:4].upper() + command = data[:4].decode("utf-8").upper() if command == '': break elif command in ['HELO','EHLO']: - self.request.sendall("250 evil.com\r\n") + self.request.sendall(b"250 evil.com\r\n") elif command in ['MAIL', 'RCPT', 'NOOP', 'RSET']: - self.request.sendall("250 OK\r\n") + self.request.sendall(b"250 OK\r\n") elif command == 'QUIT': - self.request.sendall("221 evil.com bye\r\n") + self.request.sendall(b"221 evil.com bye\r\n") elif command == "DATA": - self.request.sendall("354 start mail input, end with .\r\n") + self.request.sendall(b"354 start mail input, end with .\r\n") - mail_data = "" + mail_data = b"" while True: mail_data_chunk = self.request.recv(4096) @@ -131,17 +133,17 @@ def handle(self): mail_data += mail_data_chunk - if "\r\n.\r\n" in mail_data: + if b"\r\n.\r\n" in mail_data: break self.server.logger.info('Received mail data.') - for line in mail_data.split("\n"): + for line in mail_data.split(b"\n"): self.server.logger.info(line) - self.request.sendall("250 OK\r\n") + self.request.sendall(b"250 OK\r\n") else: - self.request.sendall("503 Command not supported\r\n") + self.request.sendall(b"503 Command not supported\r\n") except socket.timeout: self.server.logger.warning('Connection timeout') @@ -149,10 +151,10 @@ def handle(self): except socket.error as msg: self.server.logger.error('Error: %s', msg.strerror or msg) - except Exception, e: + except Exception as e: self.server.logger.error('Error: %s', e) -class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): +class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): pass ############################################################################### diff --git a/fakenet/listeners/TFTPListener.py b/fakenet/listeners/TFTPListener.py index ad42b609..ddc9517a 100644 --- a/fakenet/listeners/TFTPListener.py +++ b/fakenet/listeners/TFTPListener.py @@ -1,15 +1,17 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import logging import os import sys import threading -import SocketServer +import socketserver import socket import struct -import urllib +import urllib.request, urllib.parse, urllib.error from . import * EXT_FILE_RESPONSE = { @@ -23,11 +25,11 @@ '.txt' : 'FakeNet.txt', } -OPCODE_RRQ = "\x00\x01" -OPCODE_WRQ = "\x00\x02" -OPCODE_DATA = "\x00\x03" -OPCODE_ACK = "\x00\x04" -OPCODE_ERROR = "\x00\x05" +OPCODE_RRQ = b"\x00\x01" +OPCODE_WRQ = b"\x00\x02" +OPCODE_DATA = b"\x00\x03" +OPCODE_ACK = b"\x00\x04" +OPCODE_ERROR = b"\x00\x05" BLOCKSIZE = 512 @@ -48,7 +50,7 @@ def taste(self, data, dport): max_error_size = 5 + max_error_msg_size confidence = 1 if dport == 69 else 0 - + stripped = data.lstrip() if (stripped.startswith(OPCODE_RRQ) or stripped().startswith(OPCODE_WRQ)): @@ -82,7 +84,7 @@ def __init__(self, self.port = self.config.get('port', 69) self.logger.debug('Initialized with config:') - for key, value in config.iteritems(): + for key, value in config.items(): self.logger.debug(' %10s: %s', key, value) path = self.config.get('tftproot', 'defaultFiles') @@ -113,7 +115,7 @@ def stop(self): self.server.shutdown() self.server.server_close() -class ThreadedUDPRequestHandler(SocketServer.BaseRequestHandler): +class ThreadedUDPRequestHandler(socketserver.BaseRequestHandler): def handle(self): @@ -126,11 +128,9 @@ def handle(self): opcode = data[:2] if opcode == OPCODE_RRQ: - filename, mode = self.parse_rrq_wrq_packet(data) self.server.logger.info('Received request to download %s', filename) - - self.handle_rrq(socket, filename) + self.handle_rrq(socket, filename.decode('utf-8')) elif opcode == OPCODE_WRQ: @@ -159,7 +159,7 @@ def handle(self): self.server.logger.error('Unknown opcode: %d', struct.unpack('!H', data[:2])[0]) - except Exception, e: + except Exception as e: self.server.logger.error('Error: %s', e) raise e @@ -169,7 +169,7 @@ def handle_data(self, socket, data): if hasattr(self.server, 'filename_path') and self.server.filename_path: - safe_file = self.server.tftp_file_prefix + "_" + urllib.quote(self.server.filename_path, '') + safe_file = self.server.tftp_file_prefix + "_" + urllib.parse.quote(self.server.filename_path, '') output_file = ListenerBase.safe_join(os.getcwd(), safe_file) f = open(output_file, 'ab') @@ -195,7 +195,7 @@ def handle_rrq(self, socket, filename): # Calculate absolute path to a fake file filename_path = ListenerBase.safe_join(self.server.tftproot_path, - EXT_FILE_RESPONSE.get(file_extension.lower(), u'FakeNetMini.exe')) + EXT_FILE_RESPONSE.get(file_extension.lower(), 'FakeNetMini.exe')) self.server.logger.debug('Sending file %s', filename_path) @@ -224,16 +224,16 @@ def handle_wrq(self, socket, filename): self.server.filename_path = filename # Send acknowledgement so the client will begin writing - ack_packet = OPCODE_ACK + "\x00\x00" + ack_packet = OPCODE_ACK + b"\x00\x00" socket.sendto(ack_packet, self.client_address) def parse_rrq_wrq_packet(self, data): - filename, mode, _ = data[2:].split("\x00", 2) + filename, mode, _ = data[2:].split(b"\x00", 2) return (filename, mode) -class ThreadedUDPServer(SocketServer.ThreadingMixIn, SocketServer.UDPServer): +class ThreadedUDPServer(socketserver.ThreadingMixIn, socketserver.UDPServer): pass ############################################################################### diff --git a/fakenet/listeners/__init__.py b/fakenet/listeners/__init__.py index 74f4d0ae..84f83a73 100644 --- a/fakenet/listeners/__init__.py +++ b/fakenet/listeners/__init__.py @@ -1,15 +1,16 @@ -import ListenerBase -import RawListener -import HTTPListener -import DNSListener -import SMTPListener -import FTPListener -import IRCListener -import TFTPListener -import POPListener -import BITSListener -import ProxyListener +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + +from . import ListenerBase +from . import RawListener +from . import HTTPListener +from . import DNSListener +from . import SMTPListener +from . import FTPListener +from . import IRCListener +from . import TFTPListener +from . import POPListener +from . import ProxyListener import os -__all__ = ['ListenerBase', 'RawListener', 'HTTPListener', 'DNSListener', 'SMTPListener', 'FTPListener', 'IRCListener', 'TFTPListener', 'POPListener', 'BITSListener', 'ProxyListener'] +__all__ = ['ListenerBase', 'RawListener', 'HTTPListener', 'DNSListener', 'SMTPListener', 'FTPListener', 'IRCListener', 'TFTPListener', 'POPListener', 'ProxyListener'] diff --git a/fakenet/listeners/ssl_utils/ssl_detector.py b/fakenet/listeners/ssl_utils/ssl_detector.py index a0dfa620..0356694c 100644 --- a/fakenet/listeners/ssl_utils/ssl_detector.py +++ b/fakenet/listeners/ssl_utils/ssl_detector.py @@ -1,3 +1,5 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import logging def looks_like_ssl(data): @@ -37,20 +39,20 @@ def looks_like_ssl(data): return False # check for sslv2 which is deprecated but malware may use it anyway - if ord(data[0]) == 0x80: - if ord(data[2]) in handshake_message_types: + if data[0] == 0x80: + if data[2] in handshake_message_types: self.logger.info('SSLv2 detected') return True return False - elif ord(data[0]) not in content_types.values(): + elif data[0] not in list(content_types.values()): return False - elif ord(data[0]) == content_types['Handshake']: - return ord(data[5]) in handshake_message_types.values() + elif data[0] == content_types['Handshake']: + return data[5] in list(handshake_message_types.values()) - ssl_version = ord(data[1]) << 8 | ord(data[2]) - if ssl_version not in valid_versions.values(): + ssl_version = data[1] << 8 | data[2] + if ssl_version not in list(valid_versions.values()): return False return True diff --git a/setup.py b/setup.py index c487fb66..7edfb394 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import os import platform @@ -23,7 +25,7 @@ setup( name='FakeNet NG', - version='1.4.13', + version='3.0', description="", long_description="", author="Mandiant FLARE Team with credit to Peter Kacherginsky as the original developer", @@ -49,6 +51,6 @@ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Natural Language :: English', - "Programming Language :: Python :: 2", + 'Programming Language :: Python :: 3', ], ) diff --git a/test/CustomProviderExample.py b/test/CustomProviderExample.py index 6fa31958..04c64edf 100644 --- a/test/CustomProviderExample.py +++ b/test/CustomProviderExample.py @@ -1,3 +1,5 @@ +# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved. + import socket # To read about customizing HTTP responses, see docs/CustomResponse.md @@ -14,7 +16,7 @@ def HandleRequest(req, method, post_data=None): The HTTP post data received by calling `rfile.read()` against the BaseHTTPRequestHandler that received the request. """ - response = 'Ahoy\r\n' + response = b'Ahoy\r\n' if method == 'GET': req.send_response(200) @@ -51,7 +53,7 @@ def HandleTcp(sock): if not data: break - resp = ''.join([chr(ord(c)+1) for c in data]) + resp = b''.join([chr(c+1).encode() for c in data]) sock.sendall(resp) @@ -68,5 +70,5 @@ def HandleUdp(sock, data, addr): The host and port of the remote peer """ if data: - resp = ''.join([chr(ord(c)+1) for c in data]) + resp = b''.join([chr(c+1).encode() for c in data]) sock.sendto(resp, addr) diff --git a/test/template.ini b/test/template.ini index 3b1fc848..bf13bcc2 100644 --- a/test/template.ini +++ b/test/template.ini @@ -163,7 +163,6 @@ HostBlackList: 6.6.6.6 # * Webroot - Set webroot path for HTTPListener. # * DumpHTTPPosts - Store HTTP Post requests for the HTTPListener. # * DumpHTTPPostsFilePrefix - File prefix for the stored HTTP Post requests used by the HTTPListener. -# * BITSFilePrefix - File prefix for the stored BITS uploads used by the BITSListener. # * TFTPFilePrefix - File prefix for the stored tftp uploads used by the TFTPListener. # * DNSResponse - IP address to respond with for A record DNS queries. (DNSListener) # * NXDomains - A number of DNS requests to ignore to let the malware cycle through @@ -191,7 +190,7 @@ Enabled: True Protocol: TCP Listener: ProxyListener Port: 38926 -Listeners: HTTPListener, RawListener, FTPListener, DNSListener, POPListener, SMTPListener, TFTPListener, IRCListener, BITSListener +Listeners: HTTPListener, RawListener, FTPListener, DNSListener, POPListener, SMTPListener, TFTPListener, IRCListener Hidden: False [ProxyUDPListener] diff --git a/test/test.py b/test/test.py index 9509f84d..fa433516 100644 --- a/test/test.py +++ b/test/test.py @@ -446,8 +446,9 @@ def _testGeneric(self, label, config, tests, matchspec=[]): logger.info('Testing') logger.info('-' * 79) - passedTests = 0 - failedTests = 0 + nPassedTests = 0 + nFailedTests = 0 + failedTests = [] # Do each test for desc, (callback, args, expected) in tests.items(): @@ -460,17 +461,24 @@ def _testGeneric(self, label, config, tests, matchspec=[]): passed = self._tryTest(desc, callback, args, expected) if passed: - passedTests += 1 + nPassedTests += 1 else: - failedTests += 1 + nFailedTests += 1 + failedTests.append(desc) self._printStatus(desc, passed) time.sleep(0.5) logger.info('-' * 79) - logger.info('Tests complete') - logger.info('%s/%s tests passed, %s/%s tests failed' % (passedTests, len(tests), failedTests, len(tests))) + logger.info('Done.') + logger.info('Passed: %s/%s | Failed: %s/%s' % (nPassedTests, len(tests), nFailedTests, len(tests))) + + if nFailedTests: + logger.info('\nFailed tests:') + for test in failedTests: + logger.info('[*] %s' % (test)) + logger.info('-' * 79) if self.settings.singlehost: @@ -520,6 +528,8 @@ def _test_sk(self, proto, host, port, teststring=None, expected=None, recvd = s.recv(4096) retval = (recvd == expected) + if not retval: + logger.error('Expected response: %s, Received response: %s' % (expected, recvd)) except socket.error as e: logger.error('Socket error: %s (%s %s:%d)' % @@ -875,8 +885,8 @@ def __init__(self, startingpath, singlehost=True): self.configpath = self.genPath('%TEMP%\\fakenet.ini', '/tmp/fakenet.ini') self.stopflag = self.genPath('%TEMP%\\stop_fakenet', '/tmp/stop_fakenet') self.logpath = self.genPath('%TEMP%\\fakenet.log', '/tmp/fakenet.log') - self.fakenet = self.genPath('fakenet', 'python fakenet.py') - self.fndir = self.genPath('.', '$HOME/files/src/flare-fakenet-ng/fakenet') + self.fakenet = self.genPath('fakenet', 'python3 -m fakenet.fakenet') + self.fndir = self.genPath('.', os.path.dirname(os.getcwd())) # For process blacklisting self.pythonname = os.path.basename(sys.executable)