Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9ba0e11
Support MC2 in python
OJ Mar 21, 2026
f7aafe6
Refactor config gen
OJ Mar 24, 2026
5ec1fc2
Add support for HTTP/S PHP and TLV config
OJ Mar 24, 2026
4a5fe56
Remove emails from source
OJ Mar 24, 2026
bf781a9
Config block support for mettle
OJ Mar 26, 2026
1f0bf60
Restore android stageless via TLV
OJ May 13, 2026
1472c4d
Fix custom_headers emission, add C2 UUID for placement
OJ May 13, 2026
aa220bb
Support mc2 in transport_* commands
OJ May 13, 2026
428f34f
Add stagless payloads for Java
OJ May 13, 2026
f0dc4a7
Add debug output for incoming MC2 payloads
OJ May 19, 2026
492c030
Fix MC2 payload gen
OJ May 19, 2026
a01c8d4
Fix mettle config generation
OJ May 19, 2026
2b7e6d4
Wire MC2 into PHP payloads
OJ May 19, 2026
b60e645
Remove debug log and fix uuid loading in C2
OJ May 19, 2026
b130aa9
Add IN/OUTBOUND encoding to correctly handle encoding
OJ May 20, 2026
6c88462
Wire MC2 into mettle
OJ May 20, 2026
44b3b63
Correctly handle UUID encoding/decoding
OJ May 20, 2026
3da3c71
Move to STRING instead of RAW for UUID prefix/suffix
OJ May 20, 2026
068e9c4
Support stageless java payloads
OJ May 20, 2026
509c38e
Add support for stageless extension wiring
OJ May 20, 2026
7627ef1
Add stageless extension support to mettle
OJ May 20, 2026
45f33dc
Fallback to global UA if not present in profile
OJ Jun 8, 2026
8c1a36c
Fix connd_id to session map and C2 comment handling
OJ Jun 9, 2026
932962b
Tolerate unsupported C2 blocks and split multi-URI directives
OJ Jun 16, 2026
69a5e60
Add fetch-payload fix as per Diego's comments
OJ Jun 16, 2026
7be0277
Fix stageless Windows Meterpreter config slot
smcintyre-r7 Jun 22, 2026
dc597ca
Pull in Spencer's config block fix
OJ Jul 5, 2026
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
10 changes: 5 additions & 5 deletions db/modules_metadata_base.json
Original file line number Diff line number Diff line change
Expand Up @@ -66658,7 +66658,7 @@
"disclosure_date": null,
"type": "encoder",
"author": [
"OJ Reeves <oj@buffered.io>"
"OJ Reeves"
],
"description": "Encodes a payload using a series of SUB instructions and writing the\n encoded value to ESP. This concept is based on the known SUB encoding\n approach that is widely used to manually encode payloads with very\n restricted allowed character sets. It will not reset EAX to zero unless\n absolutely necessary, which helps reduce the payload by 10 bytes for\n every 4-byte chunk. ADD support hasn't been included as the SUB\n instruction is more likely to avoid bad characters anyway.\n\n The payload requires a base register to work off which gives the start\n location of the encoder payload in memory. If not specified, it defaults\n to ESP. If the given register doesn't point exactly to the start of the\n payload then an offset value is also required.\n\n Note: Due to the fact that many payloads use the FSTENV approach to\n get the current location in memory there is an option to protect the\n start of the payload by setting the 'OverwriteProtect' flag to true.\n This adds 3-bytes to the start of the payload to bump ESP by 32 bytes\n so that it's clear of the top of the payload.",
"references": [],
Expand Down Expand Up @@ -86684,7 +86684,7 @@
"disclosure_date": "2015-03-01",
"type": "exploit",
"author": [
"OJ Reeves <oj@beyondbinary.io>"
"OJ Reeves"
],
"description": "Some Seagate Business NAS devices are vulnerable to command execution via a local\n file include vulnerability hidden in the language parameter of the CodeIgniter\n session cookie. The vulnerability manifests in the way the language files are\n included in the code on the login page, and hence is open to attack from users\n without the need for authentication. The cookie can be easily decrypted using a\n known static encryption key and re-encrypted once the PHP object string has been\n modified.\n\n This module has been tested on the STBN300 device.",
"references": [
Expand Down Expand Up @@ -189596,7 +189596,7 @@
"author": [
"superkojiman",
"PsychoSpy <neinwechter@gmail.com>",
"OJ Reeves <oj@buffered.io>"
"OJ Reeves"
],
"description": "This module exploits a stack based buffer overflow in Ultra Mini HTTPD 1.21,\n allowing remote attackers to execute arbitrary code via a long resource name in an HTTP\n request. This exploit has to deal with the fact that the application's request handler\n thread is terminated after 60 seconds by a \"monitor\" thread. To do this, it allocates\n some RWX memory, copies the payload to it and creates another thread. When done, it\n terminates the current thread so that it doesn't crash and hence doesn't bring down\n the process with it.",
"references": [
Expand Down Expand Up @@ -207223,7 +207223,7 @@
"author": [
"Sean Dillon <sean.dillon@risksense.com>",
"Ryan Hanson",
"OJ Reeves <oj@beyondbinary.io>",
"OJ Reeves",
"Brent Cook <bcook@rapid7.com>"
],
"description": "The RDP termdd.sys driver improperly handles binds to internal-only channel MS_T120,\n allowing a malformed Disconnect Provider Indication message to cause use-after-free.\n With a controllable data/size remote nonpaged pool spray, an indirect call gadget of\n the freed channel is used to achieve arbitrary code execution.\n\n Windows 7 SP1 and Windows Server 2008 R2 are the only currently supported targets.\n\n Windows 7 SP1 should be exploitable in its default configuration, assuming your target\n selection is correctly matched to the system's memory layout.\n\n HKLM\\SYSTEM\\CurrentControlSet\\Control\\TerminalServer\\Winstations\\RDP-Tcp\\fDisableCam\n *needs* to be set to 0 for exploitation to succeed against Windows Server 2008 R2.\n This is a non-standard configuration for normal servers, and the target will crash if\n the aforementioned Registry key is not set!\n\n If the target is crashing regardless, you will likely need to determine the non-paged\n pool base in kernel memory and set it as the GROOMBASE option.",
Expand Down Expand Up @@ -311196,4 +311196,4 @@
"needs_cleanup": null,
"actions": []
}
}
}
66 changes: 56 additions & 10 deletions lib/msf/base/sessions/mettle_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,61 @@ def generate_config(opts = {})

opts[:uuid] ||= generate_payload_uuid

case opts[:scheme]
when 'http'
opts[:uri] = generate_http_uri(transport_config_reverse_http(opts))
when 'https'
opts[:uri] = generate_http_uri(transport_config_reverse_https(opts))
when 'tcp'
opts[:uri] = generate_tcp_uri(transport_config_reverse_tcp(opts))
else
raise ArgumentError, "Unknown scheme: #{opts[:scheme]}"
unless opts[:transport_config]
if opts[:stageless] == true
case opts[:scheme]
when 'http'
opts[:transport_config] = [transport_config_reverse_http(opts)]
when 'https'
opts[:transport_config] = [transport_config_reverse_https(opts)]
when 'tcp'
opts[:transport_config] = [transport_config_reverse_tcp(opts)]
else
raise ArgumentError, "Unknown scheme: #{opts[:scheme]}"
end
else
# Staged payloads inherit the stager's socket (fd transport);
# the stage must not synthesise its own C2 transport. Use an
# explicit empty array ([] is truthy, so the build is skipped).
opts[:transport_config] = []
end
end

# Generate the TLV config block
config_opts = {
ascii_str: true,
null_session_guid: opts[:stageless] == true,
expiration: (ds[:expiration] || ds['SessionExpirationTimeout']).to_i,
uuid: opts[:uuid],
transports: opts[:transport_config],
extensions: opts[:extensions] || [],
ext_format: 'bin',
mettle_platform: opts[:mettle_platform],
stageless: opts[:stageless] == true,
}.merge(meterpreter_logging_config(opts))

config = Rex::Payloads::Meterpreter::Config.new(config_opts)
opts[:config_block] = config.to_b

# Mettle reserves a fixed 8 KB slot in the binary for the config
# block. Catch the overflow here with a useful message instead of
# letting the gem's `to_binary` raise a generic "config block too
# large" — baked-in EXTENSIONS= is the usual culprit.
if opts[:config_block].length > MetasploitPayloads::Mettle::CONFIG_BLOCK_MAX
raise ArgumentError, "Mettle config block (#{opts[:config_block].length} bytes) exceeds the #{MetasploitPayloads::Mettle::CONFIG_BLOCK_MAX}-byte embedded slot. " \
"Drop EXTENSIONS= and `load <ext>` once the session is up."
end

# Keep the legacy CLI config for backward compatibility during
# transition. Skipped for staged payloads, which have no transport.
transport = opts[:transport_config].first
if transport
case opts[:scheme]
when 'http', 'https'
opts[:uri] = generate_http_uri(transport)
when 'tcp'
opts[:uri] = generate_tcp_uri(transport)
end
end

opts[:uuid] = Base64.encode64(opts[:uuid].to_raw).strip
Expand All @@ -114,7 +160,7 @@ def generate_config(opts = {})
end
opts[:session_guid] = Base64.encode64(guid).strip

opts.slice(:uuid, :session_guid, :uri, :debug, :log_file, :name, :background)
opts.slice(:uuid, :session_guid, :uri, :debug, :log_file, :name, :background, :config_block)
end

# Stage encoding is not safe for Mettle (doesn't apply to stageless)
Expand Down
95 changes: 83 additions & 12 deletions lib/msf/core/handler/reverse_http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -280,24 +280,69 @@ def setup_handler
end
end

# 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`.
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
placement = self.c2_profile&.http_post&.client&.id
else
directive = self.c2_profile&.http_get&.client&.metadata&.parameter
placement = self.c2_profile&.http_get&.client&.metadata
end

cid = nil
if placement
directive = placement.parameter
cid = request.qstring[directive[0].args[0]] if directive && directive.length > 0
unless cid
directive = self.c2_profile&.http_get&.client&.metadata&.header
directive = placement.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
cid ||= request.resource.split('?')[0].split('/').compact.last
cid = unwrap_profile_uuid(cid, placement) if cid && placement

request.conn_id = cid
end

# 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.
def unwrap_profile_uuid(candidate, placement)
prefix = placement.prepend.map{|d| d.args[0]}.join('')
suffix = placement.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

def add_response_headers(req, resp)
Expand All @@ -310,6 +355,21 @@ def add_response_headers(req, resp)
end
end

# 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.
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

#
# Removes the / handler, possibly stopping the service if no sessions are
# active on sub-urls.
Expand Down Expand Up @@ -391,7 +451,7 @@ def on_request(cli, req)
uuid.arch ||= self.arch
uuid.platform ||= self.platform

request_summary = "#{luri} with UA '#{req.headers['User-Agent']}'"
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)
Expand Down Expand Up @@ -435,7 +495,7 @@ def on_request(cli, req)
# Process the requested resource.
case info[:mode]
when :init_connect
print_status("Redirecting stageless connection from #{request_summary} to #{conn_id}")
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
Expand All @@ -447,10 +507,21 @@ def on_request(cli, req)
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
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
Expand Down
21 changes: 12 additions & 9 deletions lib/msf/core/payload/android.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,26 @@ def generate_config(opts={})
opts[:uuid] ||= generate_payload_uuid
ds = opts[:datastore] || datastore

# Session flags consumed by the Android meterpreter via TLV_TYPE_SESSION_FLAGS.
# Bit values must match Config.FLAG_* in
# java/meterpreter/shared/src/main/java/com/metasploit/stage/Config.java.
flags = 0
flags |= 1 if opts[:stageless]
flags |= 2 if ds['AndroidMeterpreterDebug']
flags |= 4 if ds['AndroidWakelock']
flags |= 8 if ds['AndroidHideAppIcon']

config_opts = {
ascii_str: true,
arch: opts[:uuid].arch,
expiration: ds['SessionExpirationTimeout'].to_i,
uuid: opts[:uuid],
transports: opts[:transport_config] || [transport_config(opts)],
stageless: opts[:stageless] == true
stageless: opts[:stageless] == true,
flags: flags
}

config = Rex::Payloads::Meterpreter::Config.new(config_opts).to_b
flags = 0
flags |= 1 if opts[:stageless]
flags |= 2 if ds['AndroidMeterpreterDebug']
flags |= 4 if ds['AndroidWakelock']
flags |= 8 if ds['AndroidHideAppIcon']
config[0] = flags.chr
config
Rex::Payloads::Meterpreter::Config.new(config_opts).to_b
end

def sign_jar(jar)
Expand Down
Loading
Loading