Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 96 additions & 33 deletions lib/msf/core/handler/reverse_http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,7 @@ def initialize(info = {})
OptString.new('HttpUserAgent',
'The user-agent that the payload should use for communication',
default: Rex::UserAgent.random,
aliases: ['MeterpreterUserAgent'],
max_length: Rex::Payloads::Meterpreter::Config::UA_SIZE - 1
aliases: ['MeterpreterUserAgent']
),
OptString.new('HttpServerName',
'The server header that the handler will send in response to requests',
Expand Down Expand Up @@ -180,28 +179,55 @@ def scheme
(ssl?) ? 'https' : 'http'
end

# The local URI for the handler.
#
# @return [String] Representation of the URI to listen on.
def luri
l = datastore['LURI'] || ""
def construct_luri(base_uri)

if l && l.length > 0
if base_uri && base_uri.length > 0
# strip trailing slashes
while l[-1, 1] == '/'
l = l[0...-1]
while base_uri[-1, 1] == '/'
base_uri = base_uri[0...-1]
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
while base_uri[-1, 1] == '/'
base_uri = base_uri[0...-1]
end
while base_uri.ends_with?('/')
base_uri.chop!
end

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer avoiding chop! as we should avoid mutating strings we don't own as it introduces bugs - and in the future there will be more frozen strings about in the future:

Ruby is implementing frozen string literals gradually over three releases:
Ruby 3.4 (Now): Opt-in warnings when you enable deprecation warnings
Ruby 3.7 (Future): Warnings enabled by default
Ruby 4.0 (Future): Frozen string literals become the default
https://www.prateekcodes.dev/ruby-34-frozen-string-literals-rails-upgrade-guide/

Which will cause issues:

3.3.0 :002 > x.chop!
(irb):2:in `chop!': can't modify frozen String: "a" (FrozenError)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

base_uri = base_uri.chop then?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No preference, the code change is just renaming an existing var so I don't mind leaving the code as-is

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, this isn't code that I wrote as part of this PR, it was me moving some code around that was already part of the codebase. Happy to tweak it though! :)

Would it not be better to use base_uri[-1] instead of .ends_with?() given it's just a single index and character comparison rather than a tail-end string comparison? Forgive my ignorance here :)


# make sure the luri has the prefix
if l[0, 1] != '/'
l = "/#{l}"
if base_uri[0, 1] != '/'
base_uri = "/#{base_uri}"
end

end

l.dup
base_uri.dup
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]
Expand Down Expand Up @@ -239,11 +265,15 @@ def setup_handler
self.service.server_name = datastore['HttpServerName']

# Add the new resource
service.add_resource((luri + "/").gsub("//", "/"),
'Proc' => Proc.new { |cli, req|
on_request(cli, req)
},
'VirtualDirectory' => true)
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
Expand All @@ -253,13 +283,37 @@ def setup_handler
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

#
# Removes the / handler, possibly stopping the service if no sessions are
# active on sub-urls.
#
def stop_handler
if self.service
self.service.remove_resource((luri + "/").gsub("//", "/"))
all_uris.each {|u|
#r = (u + "/").gsub("//", "/")
r = u.gsub("//", "/")
self.service.remove_resource(r)
}
self.service.deref
self.service = nil
end
Expand Down Expand Up @@ -314,23 +368,27 @@ def lookup_proxy_settings
def on_request(cli, req)
Thread.current[:cli] = cli
resp = Rex::Proto::Http::Response.new
info = process_uri_resource(req.relative_resource)
uuid = info[:uuid]

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

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

if info[:mode] && info[:mode] != :connect
conn_id << generate_uri_uuid(URI_CHECKSUM_CONN, uuid)
else
conn_id << req.relative_resource
conn_id = conn_id.chomp('/')
conn_id = generate_uri_uuid(URI_CHECKSUM_CONN, uuid)
end

request_summary = "#{conn_id} with UA '#{req.headers['User-Agent']}'"
conn_id.chomp!('/')

# Validate known UUIDs for all requests if IgnoreUnknownPayloads is set
if framework.db.active
Expand Down Expand Up @@ -368,16 +426,17 @@ def on_request(cli, req)
# Process the requested resource.
case info[:mode]
when :init_connect
print_status("Redirecting stageless connection from #{request_summary}")
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_URL)
pkt.add_tlv(Rex::Post::Meterpreter::TLV_TYPE_TRANS_URL, conn_id + "/")
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
Expand All @@ -386,6 +445,7 @@ def on_request(cli, req)
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
Expand All @@ -406,7 +466,7 @@ def on_request(cli, req)
end
end

create_session(cli, {
session_opts = {
:passive_dispatcher => self.service,
:dispatch_ext => [Rex::Post::Meterpreter::HttpPacketDispatcher],
:conn_id => conn_id,
Expand All @@ -416,9 +476,12 @@ def on_request(cli, req)
:retry_total => datastore['SessionRetryTotal'].to_i,
:retry_wait => datastore['SessionRetryWait'].to_i,
:ssl => ssl?,
:payload_uuid => uuid
})
: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}")
Expand Down
2 changes: 0 additions & 2 deletions lib/msf/core/opt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,9 @@ def self.http_proxy_options
),
OptString.new('HttpProxyUser', 'An optional proxy server username',
aliases: ['PayloadProxyUser'],
max_length: Rex::Payloads::Meterpreter::Config::PROXY_USER_SIZE - 1
),
OptString.new('HttpProxyPass', 'An optional proxy server password',
aliases: ['PayloadProxyPass'],
max_length: Rex::Payloads::Meterpreter::Config::PROXY_PASS_SIZE - 1
),
OptEnum.new('HttpProxyType', 'The type of HTTP proxy',
enums: ['HTTP', 'SOCKS'],
Expand Down
Loading
Loading