369 lines
No EOL
23 KiB
Python
369 lines
No EOL
23 KiB
Python
version = '0.4.2'
|
|
|
|
import json
|
|
import os
|
|
import platform
|
|
import shutil
|
|
import sys
|
|
from uuid import uuid1
|
|
from copy import deepcopy
|
|
from datetime import datetime
|
|
from traceback import print_exception
|
|
is_windows = platform.uname()[0].lower() == 'windows'
|
|
|
|
def uuid(): return str(uuid1())
|
|
|
|
class log(object):
|
|
def __init__(self, tool_name, tool_version, options={}, **params):
|
|
# record parent if child, otherwise do following setup only if parent log
|
|
self.parent = options.get('parent') # None == root, instance_parent
|
|
self.uuid = uuid() # instance_uuid
|
|
if not self.parent:
|
|
# user-controllable options
|
|
for option in ['debug', 'enabled', 'verbose']:
|
|
value = options.get(option) if isinstance(options.get(option), bool) else True
|
|
self.__setattr__(option, value)
|
|
|
|
# dispatcher depends on a common prefix being used across output files
|
|
# dispatcher will look for files dropped in directories under the log_root
|
|
self.prefix = 'concernedparent'
|
|
self.log_root = 'c:\\temp' if is_windows else '/tmp'
|
|
|
|
# create log_dir from log_root/prefix, otherwise use cwd/prefix
|
|
self.log_dir = os.path.join(self.log_root, self.prefix)
|
|
if self.enabled:
|
|
try:
|
|
if not os.path.exists(self.log_dir):
|
|
os.makedirs(self.log_dir)
|
|
except:
|
|
try:
|
|
self.log_dir = os.path.join(os.getcwd(), self.prefix)
|
|
if not os.path.exists(self.log_dir):
|
|
os.makedirs(self.log_dir)
|
|
except:
|
|
self.enabled = False
|
|
self.notify_of_error('Could not open a log output directory. Logging will be disabled.')
|
|
finally:
|
|
if self.verbose and self.enabled: print "Logging to " + self.log_dir
|
|
# setup a queue of parameters to write once next log entry
|
|
self.__queue = {}
|
|
|
|
# register the tool name, version, and extra params with the log
|
|
self._register(tool_name, tool_version, **params)
|
|
|
|
def queue(self, **params):
|
|
''' Queue any number of params for next call. Returns log. '''
|
|
try:
|
|
for key in params: self.__queue[key] = deepcopy(params[key])
|
|
except:
|
|
self.notify_of_error("Could not set queue by dictionary. Parameters were:\n"+str(params))
|
|
return self
|
|
|
|
def get(self, key, default=None):
|
|
''' Get the value of key from the params, return default if not found. '''
|
|
return self.__params.get(key, default)
|
|
|
|
def set(self, **params):
|
|
''' Set the value of params for all future calls. Returns log. '''
|
|
try:
|
|
for key in params: self.__params[key] = deepcopy(params[key])
|
|
except:
|
|
self.notify_of_error("Could not set params. Parameters were:\n"+str(params))
|
|
return self
|
|
|
|
def set_machine_info(self, hostname=None, os_name=None, os_version=None, os_arch=None, hw_processor=None, **params):
|
|
''' Set various attributes about the machine from which the log is being produced. Returns log. '''
|
|
return self.set(local_hostname=hostname, local_os_name=os_name, local_os_version=os_version, local_os_arch=os_arch, local_hw_processor=hw_processor, **params)
|
|
|
|
def make_child(self, tool_name, tool_version, parent_uuid=None, **params):
|
|
try:
|
|
child = deepcopy(self)
|
|
child.__init__(tool_name, tool_version, dict(parent=self), **params)
|
|
if parent_uuid: child.queue(event_parent_uuid=parent_uuid)
|
|
return child
|
|
except:
|
|
self.notify_of_error('Creating child log for ' + str(tool_name) + ' ' + str(tool_version))
|
|
return None
|
|
|
|
''' Tool-related helper methods. '''
|
|
def open(self, command_line=None, **params):
|
|
''' Reports this tool was opened, pass in command line if known. Returns log. '''
|
|
self('tool opened', command_line=command_line, **params)
|
|
return self
|
|
def close(self, command_line=None, results=None, status=None, success=None, **params):
|
|
''' Reports this tool was closed. '''
|
|
self.running = False
|
|
self('tool closed', command_line=command_line, command_results=results, command_status=status, command_success=success, command_uuid=uuid() if command_line else None, **params)
|
|
|
|
def command(self, name, results=None, status=None, success=None, *args, **params):
|
|
''' A tool's internal command caused activity on the remote machine. Returns event_uuid. '''
|
|
return self('command executed', command_name=name, command_args=str(args), command_results=results, command_status=status, command_success=success, command_uuid=uuid(), **params)
|
|
|
|
def launch_from_command(self, command_name, tool_name, tool_version, **params):
|
|
''' Reports that a command was run from this tool instance, spawning a child tool. Returns log. '''
|
|
parent_uuid = self.command(command_name, **params)
|
|
child = self.make_child(tool_name, tool_version, parent_uuid)
|
|
return child
|
|
|
|
def file_from_path(self, full_path, parent_uuid=None, **params):
|
|
''' Upload a file given its local path. Returns event_uuid. '''
|
|
try:
|
|
full_path = os.path.realpath(os.path.normpath(full_path))
|
|
(file_path, file_name) = os.path.split(full_path)
|
|
shutil.copyfile(full_path, self.basefilename()+'.'+file_name)
|
|
except: self.notify_of_error('Could not access file ' + full_path)
|
|
else: return self('remote file', parent_uuid, file_origin_name=file_name, file_origin_path=file_path, file_origin_created=datetime.utcfromtimestamp(os.path.getctime(full_path)).isoformat(' '), **params)
|
|
return None
|
|
|
|
def file_from_content(self, content, storage_name, parent_uuid=None, **params):
|
|
''' Upload a file given its content. Returns event_uuid. '''
|
|
try:
|
|
storage_name = storage_name or "%s.txt"%(uuid())
|
|
full_path = self.basefilename()+'.'+storage_name
|
|
with open(full_path, 'wb') as f:
|
|
f.write(content.encode('utf-8'))
|
|
except: self.notify_of_error('Could not write file ' + full_path)
|
|
else: return self('remote file', parent_uuid, file_origin_name=storage_name, file_origin_path=None, file_origin_created=datetime.utcfromtimestamp(os.path.getctime(full_path)).isoformat(' '), **params)
|
|
return None
|
|
|
|
def file_from_file(self, fd, parent_uuid=None, **params):
|
|
''' Upload a file given an open file descriptor to a local path. Returns event_uuid. '''
|
|
try: # if isinstance(fd,file)...
|
|
fd.flush()
|
|
return self.file_from_path(fd.name, parent_uuid, **params)
|
|
except:
|
|
self.notify_of_error('Could not access file descriptor for '+str(fd))
|
|
return None
|
|
|
|
''' Execution-related helper methods. '''
|
|
def start(self):
|
|
''' Queues the start time of execution. Results in stop time being marked by next log call. Returns log. '''
|
|
return self.queue(start_time=datetime.utcnow())
|
|
|
|
def execute_exploit(self, **params):
|
|
''' Intended for use by other methods (success/fail). Reports execution of an exploit from within this tool. Returns event_uuid.'''
|
|
return self('exploit executed', **params)
|
|
|
|
def execute_tool(self, **params): return self(event_type='tool executed', **params)
|
|
def successful_exploit(self, **params): return self.execute_exploit(exploit_success=True, **params)
|
|
def failed_exploit(self, **params): return self.execute_exploit(exploit_success=False, **params)
|
|
|
|
def execute_tool_from_command_line(self, command_line, **params):
|
|
''' Reports execution of this tool and the command line that started it. '''
|
|
return self.execute_tool(command_line=command_line, **params)
|
|
def successful_exploit_from_command_line(self, command_line, **params):
|
|
''' Reports successful exploitation from this tool and the command line that started it. '''
|
|
return self.successful_exploit(command_line=command_line, **params)
|
|
def failed_exploit_from_command_line(self, command_line, **params):
|
|
''' Reports failed exploitation from this tool and the command line that started it. '''
|
|
return self.failed_exploit(command_line=command_line, **params)
|
|
|
|
def successful_exploit_from_command(self, command, tool_name, tool_version, **params):
|
|
''' Ran single-fire local command to exploit with sub-tool name/version. Report successful exploit. Return event_uuid. '''
|
|
return self.launch_from_command(command, tool_name, tool_version).successful_exploit(**params)
|
|
def failed_exploit_from_command(self, command, tool_name, tool_version, **params):
|
|
''' Ran single-file local command to exploit with sub-tool name/version. Report failed exploit. Return event_uuid. '''
|
|
return self.launch_from_command(command, tool_name, tool_version).failed_exploit(**params)
|
|
|
|
''' Network-related helper methods. '''
|
|
|
|
def interface_enabled(self, ip, project=None, mac=None, name=None, **params):
|
|
''' Enable a network interface on the reporting machine. '''
|
|
return self(event_type='interface enabled', interface_ip=ip, interface_project=project, interface_mac=mac, interface_name=name, **params)
|
|
def interface_disabled(self, ip, project=None, mac=None, name=None, **params):
|
|
''' Disable a network interface on the reporting machine. '''
|
|
return self(event_type='interface disabled', interface_ip=ip, interface_project=project, interface_mac=mac, interface_name=name, **params)
|
|
|
|
def socket_opened(self, port, ip='0.0.0.0', project=None, is_tcp=None, is_udp=None, is_raw=None, **params):
|
|
''' Open a local RAW/UDP or TCP LISTENing socket. '''
|
|
if is_raw or is_tcp or is_udp: return self(event_type='socket opened', socket_port=port, socket_ip=ip, socket_project=project, socket_is_raw=is_raw, socket_is_tcp=is_tcp, socket_is_udp=is_udp, **params)
|
|
else: self.notify_of_error('Could not open socket. No socket type specified.')
|
|
def socket_closed(self, port, ip='0.0.0.0', project=None, is_tcp=None, is_udp=None, is_raw=None, **params):
|
|
''' Close a local socket and any open connections. '''
|
|
if is_raw or is_tcp or is_udp: return self(event_type='socket closed', socket_port=port, socket_ip=ip, socket_project=project, socket_is_raw=is_raw, socket_is_tcp=is_tcp, socket_is_udp=is_udp, **params)
|
|
else: self.notify_of_error('Could not close socket. No socket type specified.')
|
|
|
|
def channel_opened(self, listen_ip, listen_port, redirect_from_ip, forward_to_ip, forward_to_port, is_tcp=None, listen_project=None, redirect_from_project=None, forward_to_project=None, **params):
|
|
''' Create a channel from a listening ip:port or [ip,...]:port to a redirector, from which data is forwarded to an ip:port. '''
|
|
return self(event_type='channel opened', channel_listen_ip=listen_ip, channel_listen_port=listen_port, channel_listen_project=listen_project, channel_forward_to_ip=forward_to_ip, channel_forward_to_port=forward_to_port, channel_forward_to_project=forward_to_project, channel_redirect_from_ip=redirect_from_ip, channel_redirect_from_project=redirect_from_project, channel_is_tcp=is_tcp, **params)
|
|
def channel_closed(self, listen_ip, listen_port, listen_project=None, **params):
|
|
''' Terminate a channel from a listening ip:port or [ip,...]:port to a redirector, from which data is forwarded to an ip:port. '''
|
|
return self(event_type='channel closed', channel_listen_ip=listen_ip, channel_listen_port=listen_port, channel_listen_project=listen_project, **params)
|
|
|
|
def connection_opened(self, source_ip, source_port, destination_ip, destination_port, is_tcp=None, source_project=None, destination_project=None, **params):
|
|
''' Open (successfully) a direct connection between source (initiating) and destination (receiving) ip:port pairs. '''
|
|
return self(event_type='connection opened', connection_source_ip=source_ip, connection_source_project=source_project, connection_source_port=source_port, connection_destination_ip=destination_ip, connection_destination_project=destination_project, connection_destination_port=destination_port, connection_is_tcp=is_tcp, **params)
|
|
def connection_closed(self, source_ip, source_port, destination_ip, destination_port, is_tcp=None, source_project=None, destination_project=None, **params):
|
|
''' Close a direct connection between source (initiating) and destination (receiving) ip:port pairs. '''
|
|
return self(event_type='connection closed', connection_source_ip=source_ip, connection_source_project=source_project, connection_source_port=source_port, connection_destination_ip=destination_ip, connection_destination_project=destination_project, connection_destination_port=destination_port, connection_is_tcp=is_tcp, **params)
|
|
|
|
|
|
# UNVETTED
|
|
def connection_refused(self, source_ip, source_port, destination_ip, destination_port, is_tcp=None, source_project=None, destination_project=None, **params):
|
|
''' Connection from source ip:port to destination (listening) ip:port was refused. '''
|
|
return self(event_type='connection refused', connection_source_ip=source_ip, connection_source_project=source_project, connection_source_port=source_port, connection_destination_ip=destination_ip, connection_destination_project=destination_project, connection_destination_port=destination_port, connection_is_tcp=is_tcp, **params)
|
|
def connection_rejected(self, source_ip, source_port, destination_ip, destination_port, is_tcp=None, source_project=None, destination_project=None, **params):
|
|
''' Connection from source ip:port to destination (listening) ip:port was rejected. '''
|
|
return self(event_type='connection rejected', connection_source_ip=source_ip, connection_source_project=source_project, connection_source_port=source_port, connection_destination_ip=destination_ip, connection_destination_project=destination_project, connection_destination_port=destination_port, connection_is_tcp=is_tcp, **params)
|
|
def connection_failed(self, source_ip, source_port, destination_ip, destination_port, is_tcp=None, source_project=None, destination_project=None, **params):
|
|
''' Connection from source ip:port to destination (listening) ip:port failed. '''
|
|
return self(event_type='connection failed', connection_source_ip=source_ip, connection_source_project=source_project, connection_source_port=source_port, connection_destination_ip=destination_ip, connection_destination_project=destination_project, connection_destination_port=destination_port, connection_is_tcp=is_tcp, **params)
|
|
def trigger_sent(self, trigger_type, source_ip, source_port, target_ip, target_port, is_tcp=None, **params):
|
|
''' Send a trigger from source ip:port at a target (probably locally listening) ip:port. '''
|
|
return self(event_type='trigger sent', trigger_source_ip=source_ip, trigger_source_port=source_port, trigger_target_ip=target_ip, trigger_target_port=target_port, trigger_type=trigger_type, **params)
|
|
|
|
|
|
# Library Functionality
|
|
def basefilename(self):
|
|
return os.path.join(self.log_dir, self.prefix) + '.' + datetime.utcnow().strftime('%Y%m%d%H%M%S') + '.' + self.get('tool_name').lower()
|
|
|
|
def notify_of_warning(self,warning_string=''):
|
|
if self.enabled and self.verbose:
|
|
print "Warning:", warning_string
|
|
if self.debug:
|
|
exception_type, exception_value, exception_traceback = sys.exc_info()
|
|
print_exception(exception_type, exception_value, exception_traceback, limit=10, file=sys.stdout)
|
|
|
|
def notify_of_error(self,error_string=''):
|
|
if self.enabled:
|
|
if self.verbose: print "Error:", error_string
|
|
if self.debug:
|
|
exception_type, exception_value, exception_traceback = sys.exc_info()
|
|
if self.verbose: print_exception(exception_type, exception_value, exception_traceback, limit=10, file=sys.stdout)
|
|
else:
|
|
try:
|
|
with open(self.basefilename()+'.error', 'a') as f:
|
|
f.write('Error: '+str(error_string)+'\n')
|
|
print_exception(exception_type, exception_value, exception_traceback, limit=10, file=f)
|
|
self(event_type='error file', file_origin_name=f.name[f.name.rfind(self.get('tool_name').lower()):], file_origin_path=os.path.dirname(f.name), file_origin_created=datetime.utcfromtimestamp(os.path.getctime(f.name)).isoformat(' '))
|
|
except:
|
|
print 'Notice: The following is a notification of something gone awry and should not impact you operationally.'
|
|
print 'Please save the following traceback and inform the developer.\nError:', error_string
|
|
print_exception(exception_type, exception_value, exception_traceback, limit=10, file=sys.stdout)
|
|
exception_type, exception_value, exception_traceback = sys.exc_info()
|
|
print_exception(exception_type, exception_value, exception_traceback, limit=10, file=sys.stdout)
|
|
|
|
def _register(self, tool_name, tool_version, **params):
|
|
try:
|
|
def_pars = {'tool_name': str(tool_name) if tool_name else 'Unknown',
|
|
'tool_version': str(tool_version) if tool_version else None,
|
|
'instance_uuid': self.uuid,
|
|
'instance_parent_uuid': self.parent.uuid if self.parent else None,
|
|
'instance_log_version': version,
|
|
}
|
|
except:
|
|
def_pars = {'tool_name': 'Unknown', 'tool_version': None, 'instance_uuid': uuid(), 'instance_parent_uuid': None, 'instance_log_version': version}
|
|
self.notify_of_warning('Tool parameters invalid. Moving on.')
|
|
try:
|
|
self.__params = deepcopy(params)
|
|
for k in def_pars:
|
|
if not self.__params.has_key(k):
|
|
self.__params[k] = def_pars[k]
|
|
except:
|
|
self.__params = def_pars
|
|
self.notify_of_warning('{params} passed were invalid and have been reset. Moving on.')
|
|
|
|
def __contains__(self,element):
|
|
try: return True if self[element] else True
|
|
except AttributeError: return False
|
|
|
|
def __getitem__(self,key):
|
|
return self.__getattribute__(key) # removed __getattr__(key) since get/setattr no longer defined
|
|
|
|
def __setitem__(self,key,value):
|
|
self.__setattr__(key, value)
|
|
|
|
def __call__(self, event_type='heartbeat', parent_uuid=None, event_time=None, **params):
|
|
''' Basic log call. At a minimum, this will write the tool name and version specified during init,
|
|
an event_type (heartbeat if not specified), a parent event (only if specified), and an event_time
|
|
(now if not specified). '''
|
|
try:
|
|
# build output dictionary
|
|
d = self._flatten(event_time=event_time if event_time else datetime.utcnow(),
|
|
event_type=event_type,
|
|
event_uuid=uuid(),
|
|
event_parent_uuid=parent_uuid,
|
|
**params)
|
|
|
|
# mark stop time and replace event time with start time
|
|
if d.get('start_time'):
|
|
d['stop_time'] = d['event_time']
|
|
d['event_time'] = d['start_time']
|
|
|
|
# write/append JSON entry to file
|
|
if self.enabled:
|
|
try:
|
|
filename = self.basefilename() + '.json'
|
|
with open(filename, 'a') as f:
|
|
f.write(self._dumps(d)+'\n')
|
|
except:
|
|
self.notify_of_error("Failed to generate output file. Parameters were:\n"+str(params))
|
|
try: os.remove(filename)
|
|
except: pass
|
|
|
|
return d['event_uuid']
|
|
except:
|
|
self.notify_of_error("Key error. Parameters were:\n"+str(params))
|
|
|
|
def _flatten(self, **params):
|
|
try:
|
|
ret = self.__params.copy()
|
|
for k in self.__queue:
|
|
ret[k] = self.__queue[k]
|
|
self.__queue = {}
|
|
for k in params:
|
|
ret[k] = deepcopy(params[k])
|
|
return ret
|
|
except:
|
|
self.notify_of_error("Problem flattening log. Parameters so far were:\n"+str(ret)+'\nParameters passed were:\n'+str(params))
|
|
return {}
|
|
|
|
def _dumps(self, obj):
|
|
return json.dumps(obj, cls=self._dtencoder)
|
|
|
|
class _dtencoder(json.JSONEncoder):
|
|
def default(self, obj):
|
|
if isinstance(obj, datetime): return obj.isoformat(' ')
|
|
return json.JSONEncoder.default(self, obj)
|
|
|
|
"""
|
|
def __getattr__(self,name):
|
|
return object.__getattribute__(self,lower(name))
|
|
|
|
def __setattr__(self,name,value):
|
|
object.__setattr__(self, lower(name), value)
|
|
if isinstance(self.__getattr__(lower(name)), self.baselogtype):
|
|
self.__getattr__(name).log = self
|
|
self.__getattr__(name).name = lower(name)
|
|
|
|
import threading
|
|
def pacemaker(self, timeout=60):
|
|
# This is a stand-alone heartbeat generator. To pulse from your own control loop,
|
|
# call your AbstractLog subclass instance event handler (e.g. AbstractLog['event']()
|
|
def __target(timeout=60):
|
|
if platform.uname()[0].lower() == "windows":
|
|
import win32con
|
|
import win32event
|
|
self.running = True
|
|
kill = win32event.CreateEvent(None, 1, 0, None)
|
|
pulse = win32event.CreateWaitableTimer(None, 0, None)
|
|
win32event.SetWaitableTimer(pulse, 0, timeout*1000, None, None, False)
|
|
while(self.running):
|
|
try:
|
|
result = win32event.WaitForMultipleObjects([kill, pulse], False, 1000)
|
|
|
|
# if kill signal received, break loop
|
|
if(result == win32con.WAIT_OBJECT_0): break
|
|
# elif timeout has passed, generate a pulse
|
|
elif(result == win32con.WAIT_OBJECT_0 + 1): self['event']()
|
|
except:
|
|
self.notify_of_error("Pacemaker shutdown. Heartbeats will not be generated.")
|
|
win32event.SetEvent(kill)
|
|
elif self.verbose: print "Pacemaker only supported in Windows at this time. "
|
|
|
|
try:
|
|
self.thread = threading.Thread(target=__target, args=(timeout,) )
|
|
#self.thread.start() NOT READY FOR DEPLOYMENT
|
|
except:
|
|
self.notify_of_error("Pacemaker thread exception. Heartbeats will not be generated.")
|
|
""" |