Skip to content

Stageless PHP/Python/Java/Windows/Mettle with Malleable C2 profile support#21483

Open
OJ wants to merge 22 commits into
rapid7:6.5from
OJ:feature/mc2-all
Open

Stageless PHP/Python/Java/Windows/Mettle with Malleable C2 profile support#21483
OJ wants to merge 22 commits into
rapid7:6.5from
OJ:feature/mc2-all

Conversation

@OJ
Copy link
Copy Markdown
Contributor

@OJ OJ commented May 20, 2026

Malleable C2 for all meterpreter runtimes (PHP/Python/Java/Mettle/Windows)

Builds on this previous PR to bring full MC2 + stageless feature parity to every meterpreter, plus a few fixes the wider work uncovered.

What's in here

MC2 wiring for non-Windows meterpreters

Mettle, PHP, Python, and Java now honour a Malleable C2 profile end-to-end:

  • Per-verb set uri "/foo" overrides LURI for that verb's requests; the framework registers each profile URI alongside LURI in all_uris so requests land on the right handler.
  • client.metadata / client.id placement (parameter, header, cookie, default path-append) emit TLV_TYPE_C2_UUID_GET/HEADER/COOKIE so payloads place the UUID where the profile says.
  • prepend, base64/base64url, and append directives on those placements work — the framework emits TLV_TYPE_C2_ENC_UUID + TLV_TYPE_C2_UUID_PREFIX + TLV_TYPE_C2_UUID_SUFFIX; each payload wraps the UUID accordingly; the handler reverses it.

Inbound vs outbound encoding split

TLV_TYPE_C2_ENC split into three independent settings — all carried in both TLV_TYPE_C2_GET and TLV_TYPE_C2_POST groups:

TLV Direction Sourced from
TLV_TYPE_C2_ENC_INBOUND server → client body decode server { output { base64|base64url } }
TLV_TYPE_C2_ENC_OUTBOUND client → server body encode client { output { base64|base64url } } (POST only)
TLV_TYPE_C2_ENC_UUID UUID placement encoding client { metadata|id { base64|base64url } }

Plus TLV_TYPE_C2_UUID_PREFIX/SUFFIX for the prepend/append directives on those placement sections.

Old TLV_TYPE_C2_ENC is gone; staged C-meterpreter, mettle, java, php, python, framework all use the split. TLV_TYPE_TRANS_* were unused and removed — the python extension's transport.py is stubbed with NotImplementedError pending a rewrite onto the C2 TLVs, this might be removed entirely as it's probably not a feature that anyone uses anyway.

UUID handling consistency

All four runtimes now follow the same pattern (matching metsrv generate_uri):

  • TLV_TYPE_C2_UUID carries the current connection's UUID (set initially via generate_uri_uuid(URI_CHECKSUM_INIT_CONN, uuid) in config.rb).
  • COMMAND_ID_CORE_PATCH_UUID (renamed from _PATCH_URL) updates the in-memory UUID without mutating the base URL.
  • Each request rebuilds the URL from scheme://host:port + profile uri + current rendered UUID.
  • For non-MC2 mode, the LURI path is preserved.

Java stageless overhaul

  • Single Java meterpreter modules now produce a self-contained jar (Main-Class: StagelessMain, full meterpreter + JarFileClassLoader + config-block resource embedded). msfvenom -f jar now routes through the modules' generate_jar override rather than the inherited stager-jar path.
  • loadExtension works under either bootstrap mode — lazily creates a JarFileClassLoader (and reuses it across loads) when the JVM-supplied AppClassLoader is in play.

Stageless EXTENSIONS= support

Every stageless single (Python/PHP/Java/Mettle, plus the existing Windows path) now bakes the listed extensions into the config block and hot-loads them before the C2 dispatch loop starts. The framework picks the right on-disk format via a new :ext_format opt ('x86.dll'/'x64.dll'/'jar'/'py'/'php'/'bin') so non-Windows extensions skip the RDI prep and are shipped as-is (with the gem's encrypted-payload short-circuit honoured for everything that has one).

Mettle's 8 KB config-block reservation can't hold real extensions; that's now caught at generation time with a clear error pointing the user at runtime load <ext> instead. EXTENSIONS=stdapi is a no-op for mettle (stdapi is already linked into mettle.bin).

Misc fixes / refactors found along the way

  • reverse_http.rb#request_summary now logs req.resource (full request URI) instead of luri — much more useful when MC2 routes hit /<profile-uri>/<uuid> mount points.
  • reverse_http.rb#find_resource_id rewritten to derive the candidate UUID from either query parameter, header, or path segment, then reverse profile-side prepend/append + base64 before handing it to process_uri_resource. Conservative: returns the candidate untouched if a declared wrapper isn't present (instead of mangling and feeding garbage downstream).

Validated end-to-end

runtime MC2 (body + UUID transforms) stageless EXTENSIONS=
PHP ✔ (e.g. stdapi)
Python
Java
Mettle (x86/x64 linux) warn-and-skip for things baked into the binary; honest error when the config block would overflow
Windows native (x86/x64) unchanged ✔ (existing path, now using :ext_format)

Notes for review

  • Other linux mettle archs (aarch64, arm*, mips*, ppc*, zarch) got the same module-side wiring as x86/x64 but I couldn't physically test them.
  • metasploit-payloads and mettle repos both have matching pull requests.

Testing

In short, for PHP, Python, Windows, Mettle and Java:

  • Load every combination of reverse http/https/tcp, bind tcp staged and make sure they work with the usual configurations.
  • Load every combination of stageless payload and make sure they work as expected, including with loading extra extensions.
  • Load every combination of stageless payload and use MALLEABLEC2 to use a C2 profile file (like this).

Build and run the payloads with the correct associated handlers. Good luck. Goodspeed. The testing matrix here is huge.

NOTE: I have not been able to test Android Meterpreter, so I will need help with this one.

NOTE: Merge is targeting 6.5 branch.

OJ added 22 commits March 22, 2026 14:21
Comms handle is a windows concern, so that's removed from the generation
of the config TLV now.
They aren't valid any more.
Stagless android was busted when meterp switched to TLV config. This
moves the flags to a TLV instead of the first byte of the config block.
Primary fix here is to make sure we put the UUID in the configuration in
the right format. It was being ignored by Windows, but not by python.
Properly supports stageless now.
PHP, java, python, and php. Adjusting windows to support.

Mettle yet to come.
@smcintyre-r7

This comment was marked as resolved.

@cdelafuente-r7 cdelafuente-r7 moved this from Todo to In Progress in Metasploit Kanban May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants