From a4bf869a14b3a783c96eeae48f74e786f0dba5a5 Mon Sep 17 00:00:00 2001 From: Antony Saba Date: Tue, 10 Oct 2017 19:08:46 -0400 Subject: [PATCH] Fixes to prevent arbitrary file system traversal --- fakenet/configs/default.ini | 3 ++ fakenet/listeners/BITSListener.py | 66 ++++++++++++------------------- fakenet/listeners/HTTPListener.py | 51 ++++++++++++------------ fakenet/listeners/TFTPListener.py | 33 +++++++++++++--- fakenet/listeners/__init__.py | 40 ++++++++++++++++++- 5 files changed, 119 insertions(+), 74 deletions(-) diff --git a/fakenet/configs/default.ini b/fakenet/configs/default.ini index 7326b57..606ab1f 100644 --- a/fakenet/configs/default.ini +++ b/fakenet/configs/default.ini @@ -163,6 +163,8 @@ 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 # all of the backup C2 servers. (DNSListener) @@ -315,6 +317,7 @@ Protocol: UDP Listener: TFTPListener TFTPRoot: defaultFiles/ Hidden: False +TFTPFilePrefix: tftp [POPServer] Enabled: True diff --git a/fakenet/listeners/BITSListener.py b/fakenet/listeners/BITSListener.py index a219302..4f5a97a 100644 --- a/fakenet/listeners/BITSListener.py +++ b/fakenet/listeners/BITSListener.py @@ -13,13 +13,16 @@ import socket import posixpath -import mimetypes import time +import urllib + from BaseHTTPServer import HTTPServer from SimpleHTTPServer import SimpleHTTPRequestHandler +import fakenet.listeners + # BITS Protocol header keys K_BITS_SESSION_ID = 'BITS-Session-Id' @@ -350,8 +353,8 @@ def _handle_create_session(self): # case mutual supported protocol is found if protocols_intersection: headers[K_BITS_PROTOCOL] = list(protocols_intersection)[0] - requested_path = self.path[1:] if self.path.startswith("/") else self.path - absolute_file_path = os.path.join(self.server.config.get('webroot','defaultFiles'), requested_path) + safe_path = self.server.bits_file_prefix + '_' + urllib.quote(self.path, '') + absolute_file_path = fakenet.listeners.safe_join(os.getcwd(), safe_path) session_id = self.__get_current_session_id() self.server.logger.info("Creating BITS-Session-Id: %s", session_id) @@ -416,14 +419,7 @@ def do_BITS_POST(self): repr(e.internal_exception)) self.__send_response(headers, status_code = status_code) -class HTTPListener(): - - if not mimetypes.inited: - mimetypes.init() # try to read system mime.types - extensions_map = mimetypes.types_map.copy() - extensions_map.update({ - '': 'text/html', # Default - }) +class BITSListener(): def __init__(self, config={}, name='BITSListener', logging_level=logging.DEBUG, running_listeners=None): @@ -444,46 +440,28 @@ def __init__(self, config={}, name='BITSListener', for key, value in config.iteritems(): self.logger.debug(' %10s: %s', key, value) - # Initialize webroot directory - self.webroot_path = self.config.get('webroot','defaultFiles') - - # Try absolute path first - if not os.path.exists(self.webroot_path): - - # Try to locate the webroot directory relative to application path - self.webroot_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), self.webroot_path) - - if not os.path.exists(self.webroot_path): - self.logger.error('Could not locate webroot directory: %s', self.webroot_path) - sys.exit(1) + 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 - self.server.webroot_path = self.webroot_path - self.server.extensions_map = self.extensions_map if self.config.get('usessl') == 'Yes': self.logger.debug('Using SSL socket.') - keyfile_path = 'privkey.pem' - if not os.path.exists(keyfile_path): - keyfile_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), keyfile_path) - - if not os.path.exists(keyfile_path): - self.logger.error('Could not locate privkey.pem') - sys.exit(1) - - certfile_path = 'server.pem' - if not os.path.exists(certfile_path): - certfile_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), certfile_path) + keyfile_path = fakenet.listeners.abs_config_path('privkey.pem') + if keyfile_path is None: + self.logger.error('Could not locate privkey.pem') + sys.exit(1) - if not os.path.exists(certfile_path): - self.logger.error('Could not locate certfile.pem') - sys.exit(1) + certfile_path = fakenet.listeners.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') @@ -501,11 +479,17 @@ def test(config): pass def main(): + """ + Run from the flare-fakenet-ng root dir with the following command: + + python2 -m fakenet.listeners.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', 'webroot': '../defaultFiles' } + config = {'port': '80', 'usessl': 'No' } - listener = HTTPListener(config) + listener = BITSListener(config) listener.start() ########################################################################### diff --git a/fakenet/listeners/HTTPListener.py b/fakenet/listeners/HTTPListener.py index 61d4b8f..a588fb1 100644 --- a/fakenet/listeners/HTTPListener.py +++ b/fakenet/listeners/HTTPListener.py @@ -15,6 +15,7 @@ import time +import fakenet.listeners MIME_FILE_RESPONSE = { 'text/html': 'FakeNet.html', @@ -68,24 +69,19 @@ def __init__( self.name = 'HTTP' self.port = self.config.get('port', 80) - self.logger.info('Starting...') + self.logger.info('Initializing...') self.logger.debug('Initialized with config:') for key, value in config.iteritems(): self.logger.debug(' %10s: %s', key, value) # Initialize webroot directory - self.webroot_path = self.config.get('webroot','defaultFiles') - - # Try absolute path first - if not os.path.exists(self.webroot_path): + path = self.config.get('webroot','defaultFiles') + self.webroot_path = fakenet.listeners.abs_config_path(path) + if self.webroot_path is None: + self.logger.error('Could not locate webroot directory: %s', path) + sys.exit(1) - # Try to locate the webroot directory relative to application path - self.webroot_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), self.webroot_path) - - if not os.path.exists(self.webroot_path): - self.logger.error('Could not locate webroot directory: %s', self.webroot_path) - sys.exit(1) def start(self): self.logger.debug('Starting...') @@ -101,26 +97,22 @@ def start(self): self.logger.debug('Using SSL socket.') keyfile_path = 'listeners/ssl_utils/privkey.pem' - if not os.path.exists(keyfile_path): - keyfile_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), keyfile_path) - - if not os.path.exists(keyfile_path): - self.logger.error('Could not locate privkey.pem') - sys.exit(1) + keyfile_path = fakenet.listeners.abs_config_path(keyfile_path) + if keyfile_path is None: + self.logger.error('Could not locate %s', keyfile_path) + sys.exit(1) certfile_path = 'listeners/ssl_utils/server.pem' - if not os.path.exists(certfile_path): - certfile_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), certfile_path) - - if not os.path.exists(certfile_path): - self.logger.error('Could not locate certfile.pem') - sys.exit(1) + certfile_path = fakenet.listeners.abs_config_path(certfile_path) + if certfile_path is None: + self.logger.error('Could not locate %s', certfile_path) + 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() + self.server_thread.start() def stop(self): self.logger.info('Stopping...') @@ -236,7 +228,8 @@ def get_response(self, path): _, ext = posixpath.splitext(path) response_type = self.server.extensions_map.get(ext, 'text/html') - response_filename = os.path.join(self.server.webroot_path, path[1:]) + # Do after checking for trailing '/' since normpath removes it + response_filename = fakenet.listeners.safe_join(self.server.webroot_path, path) # Check the requested path exists if not os.path.exists(response_filename): @@ -293,9 +286,15 @@ def test(config): print '-'*80 def main(): + """ + Run from the flare-fakenet-ng root dir with the following command: + + python2 -m fakenet.listeners.HTTPListener + + """ 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', 'webroot': '../defaultFiles' } + config = {'port': '8443', 'usessl': 'Yes', 'webroot': 'fakenet/defaultFiles' } listener = HTTPListener(config) listener.start() diff --git a/fakenet/listeners/TFTPListener.py b/fakenet/listeners/TFTPListener.py index edcd338..e19d8ba 100644 --- a/fakenet/listeners/TFTPListener.py +++ b/fakenet/listeners/TFTPListener.py @@ -9,6 +9,9 @@ import socket import struct +import urllib +import fakenet.listeners + EXT_FILE_RESPONSE = { '.html': 'FakeNet.html', '.png' : 'FakeNet.png', @@ -92,7 +95,14 @@ def start(self): self.server.logger = self.logger self.server.config = self.config - self.server.tftproot_path = self.config.get('tftproot', 'defaultFiles') + + path = self.config.get('tftproot', 'defaultFiles') + self.server.tftproot_path = fakenet.listeners.abs_config_path(path) + if self.server.tftproot_path is None: + self.logger.error('Could not locate tftproot directory: %s', path) + sys.exit(1) + + self.server.bits_file_prefix = self.config.get('tftpfileprefix', 'tftp') self.server_thread = threading.Thread(target=self.server.serve_forever) self.server_thread.daemon = True @@ -141,7 +151,10 @@ def handle(self): if hasattr(self.server, 'filename_path') and self.server.filename_path: - f = open(self.server.filename_path, 'ab') + safe_file = self.server.tftp_file_prefix + "_" + urllib.quote(self.server.filename_path, '') + output_file = fakenet.listeners.safe_join(os.getcwd(), + safe_file) + f = open(output_file, 'ab') f.write(data[4:]) f.close() @@ -169,7 +182,8 @@ def handle(self): def handle_rrq(self, socket, filename): - filename_path = os.path.join(self.server.tftproot_path, filename) + filename_path = fakenet.listeners.safe_join(self.server.tftproot_path, + filename) # If virtual filename does not exist return a default file based on extention if not os.path.isfile(filename_path): @@ -177,7 +191,8 @@ def handle_rrq(self, socket, filename): file_basename, file_extension = os.path.splitext(filename) # Calculate absolute path to a fake file - filename_path = os.path.join(self.server.tftproot_path, EXT_FILE_RESPONSE.get(file_extension.lower(), u'FakeNetMini.exe')) + filename_path = fakenet.listeners.safe_join(self.server.tftproot_path, + EXT_FILE_RESPONSE.get(file_extension.lower(), u'FakeNetMini.exe')) self.server.logger.debug('Sending file %s', filename_path) @@ -203,7 +218,7 @@ def handle_rrq(self, socket, filename): def handle_wrq(self, socket, filename): - self.server.filename_path = os.path.join(self.server.tftproot_path, filename) + self.server.filename_path = filename # Send acknowledgement so the client will begin writing ack_packet = OPCODE_ACK + "\x00\x00" @@ -224,9 +239,15 @@ def test(config): pass def main(): + """ + Run from the flare-fakenet-ng root dir with the following command: + + python2 -m fakenet.listeners.TFTPListener + + """ logging.basicConfig(format='%(asctime)s [%(name)15s] %(message)s', datefmt='%m/%d/%y %I:%M:%S %p', level=logging.DEBUG) - config = {'port': '69', 'protocol': 'udp', 'tftproot': '../defaultFiles'} + config = {'port': '69', 'protocol': 'udp', 'tftproot': 'defaultFiles'} listener = TFTPListener(config) listener.start() diff --git a/fakenet/listeners/__init__.py b/fakenet/listeners/__init__.py index 41beb16..66b5647 100644 --- a/fakenet/listeners/__init__.py +++ b/fakenet/listeners/__init__.py @@ -9,5 +9,43 @@ import BITSListener import ProxyListener -__all__ = ['RawListener', 'HTTPListener', 'DNSListener', 'SMTPListener', 'FTPListener', 'IRCListener', 'TFTPListener', 'POPListener', 'BITSListener', 'ProxyListener'] +import os +__all__ = ['safe_join', 'abs_config_path', 'RawListener', 'HTTPListener', 'DNSListener', 'SMTPListener', 'FTPListener', 'IRCListener', 'TFTPListener', 'POPListener', 'BITSListener', 'ProxyListener'] + +def safe_join(root, path): + """ + Joins a path to a root path, even if path starts with '/', using os.sep + """ + + # prepending a '/' ensures '..' does not traverse past the root + # of the path + if not path.startswith('/'): + path = '/' + path + normpath = os.path.normpath(path) + + return root + normpath + +def abs_config_path(path): + """ + Attempts to return the absolute path of a path from a configuration + setting. + + First tries just to just take the abspath() of the parameter to see + if it exists relative to the current working directory. If that does + not exist, attempts to find it relative to the 'fakenet' package + directory. Returns None if neither exists. + """ + + # Try absolute path first + abspath = os.path.abspath(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 os.path.exists(relpath): + return os.path.abspath(relpath) + + return None