Stageless Mettle with Malleable C2 profile support#294
Conversation
Replace the CLI-argument-only configuration path with a TLV config block that the framework patches into the binary at generation time. This brings mettle in line with the Windows, Python, Java, and PHP Meterpreter implementations that all use Rex::Payloads::Meterpreter::Config. - Add C2 TLV constants (700-725 series) to tlv_types.h - Add 8KB CONFIG_BLOCK placeholder in main.c with signature-based patching, checked before CLI args and injection detection at startup - Add tlv_packet_from_raw() to tlv.c for wrapping GROUP TLV children - Parse UUID, session GUID, session expiry, debug log, and C2 transport groups from the config packet - Add c2_transport_config and c2_verb_config structs to c2.h - Add c2_add_transport_uri_config() to attach parsed config to transports - Parse C2 GET/POST profile sub-groups including URI, encoding flags, prefix/suffix, prefix/suffix skip, and UUID placement options - Implement profile-aware HTTP transport: per-verb URL building, Base64/Base64URL encode/decode, prefix/suffix wrapping on egress, prefix/suffix stripping and decoding on ingress, UUID in query param/header/cookie - Apply TLV transport config (UA, custom headers) during HTTP transport init alongside legacy pipe-separated URI args - Update mettle.rb to patch CONFIG_BLOCK into binaries - Fix json-c calloc argument order for modern GCC The legacy DEFAULT_OPTS CLI path is preserved as a fallback.
dledda-r7
left a comment
There was a problem hiding this comment.
Did the first iteration here, i will start playing with mettle and also give you some datapoint regarding other architectures
ResolvedHello @OJ, I started the testing and I don't get back any session. I tried both with a profile and no profile. No Profile setDatastore OptionsMetasploit Outputand continue spamming that. I remember Windows 7 was having the same result... might be some URL patching issue. |
|
Hi @dledda-r7 ! Thank you for looking at this. Strange that you're having the issue as I was able to get sessions in each case. I'll dive into this very shortly and get back to you. Thanks again. |
|
Just did a local test: Then in the other console: This seems to work fine with both http and https. Can you please share a copy of the profile you're using? Thank you! |
|
Oh I see you're not using one! My bad. I just fired it up without the C2 profile set, and I had the same issue. Looking into it now. Thanks! |
|
Should be back on track now, I'll address your other points now. Thank you. |
ResolvedHi @OJ, while testing the fix for the non-profile, i also will share the profile that is making issues: this is the same i used for the Windows C2 test |
|
This should be good now mate. |
|
I added to the framework side as well, so grab that too please. |
|
I am stil getting the same issue, will try to investigate more |
|
Generation/exec: Listener: This includes changes to mettle source, mettle gem/ruby, and MSF. |
|
@OJ sorry my bad, i forgot to reinstall the gem for the w00t w00t gonna try with the other arches |
Test PlanProfiles Base: https://github.com/BC-SECURITY/Malleable-C2-Profiles/tree/master/Normal X64reverse tcp (stageless and staged)stageless ✅staged ✅reverse http (stageless) ✅reverse https (stageless) ✅test at least 15 profilesamazon.profile ✅bing_maps ✅bingsearch_getonly 🔴microsoft_update ✅mscrl ✅msnbcvideo_getonly ✅office365_calendar ✅oscp ✅reddit ✅rtmp ✅slack 🔴stackoverflow ✅trevor ✅youtube_video ✅zoom ✅X86reverse tcp (stageless ad staged)reverse https (stageless)test profilesARMLEreverse tcp (stageless ad staged)staged ✅stageless ✅reverse https (stageless) ✅profile test ✅AARCH64reverse tcp (stageless ad staged)staged ✅stageless ✅reverse https (stageless) ✅test profileszoom ✅office364_calendar ✅rtmp ✅MIPSLEreverse tcp (staged)reverse https (stageless)test profilesMIPS64reverse tcp (stageless)reverse https (stageless)test profilesMacOS testingTBD |
|
Hello @OJ, I am testing the profiles and the other arches, i noticed the staged TCP seems broken, can you check it on your side? update: also some profile are not working, I will start triaging them slack diff filebingsearch_getonly diff filefixed slack profilebingsearch_getonly fixed file |
Staged payloads embed the standard session TLVs (UUID, session GUID, expiry) in the config block but never include a C2 group — they inherit the stager's already-connected socket via argv "m <fd>". Previously, parse_config_block returned 0 as long as the TLV blob parsed, so the "argv[0] == 'm'" fallback in main() never ran and mettle sat in its event loop with no transport registered. Track how many transports get added from C2 groups while iterating, and if none were added, free the parsed packet and return -1. The fd-based branch then wires up "fd://<n>" as expected. UUID and session GUID extracted earlier in the function are left on the dispatcher; the fallback path's parse_default_args sets them again from the embedded DEFAULT_OPTS cmdline, so staged sessions get the right identity.
|
Hi @dledda-r7 ! Thanks again for the continued testing mate. I've just pushed up a change that should resolve the staged payloads. That was a silly mistake, and I though I had covered it already. Please let me know if that works for you. Cheers. |
|
Hey @OJ! Ok I will test it asap. |
|
@dledda-r7 the "print" statement is effectively always supported because it just means "render". So we don't have any code that checks for that keyword because it should always output that result in the correct spot. Which is the best profile to use to validate this? The "broken" Slack one? |
PHP TestingTCPStagedstagelesshttpstagelessamazon.profilebing_maps.profilebingsearch_getonly.profilemicrosof_update.profilemscrlmsnbcvideo_getonly.profileoffice365.profileoscp.profilereddit.profilertmp.profileslackstackoverflowtrevoryoutube.profilezoom.profilestagedhttpsstagelessamazon.profilebing_maps.profilebingsearch_getonly.profilemicrosoft_update.profilemscrlmsnbcvideo_getonly.profileoffice365.profileoscp.profilereddit.profilertmp.profileslackstackoverflowtrevor.profileyoutube.profilezoom.profilestaged |
PythonTCPStaggedStagelessHTTPstagelessamazon.profilebing_mapsbingsearch_getonlymicrosoft_update_getonlymscrlmsnbc_videooffice365_calendaroscprmtpSlackStackoverflowTrevorYoutubeZoombroken Staggedhttpsstagelessamazon.profilebing_maps.profilebingsearch_getonly.profilemicrosoft_update.profilemscrl.profilemsnbcvideo_getonly.profileoffice365_calendar.profileoscp.profilereddit.profilertmp.profileslack.profilestackoverflow.profiletrevor.profileyoutube_video.profilezoom.profilebroken |
|
Hello @OJ, tested PHP and Python - PHP looks good, for Python, zoom.profile does not receive a session: |
|
@msutovsky-r7 thank you! I can see a few things in that zoom profile that aren't going to be supported (at least in the short term), such as |
Hello OJ! Current open issue I spotted: slack profile doesn't work on mettle, sessions doesn't open (You can get them by the link I put on the start of the test plan) I copied only the http-get and http-post section Looks like the http server is Broken and the fetch payloads are not working anymore
|
|
@OJ I have triged the issue with fetch payload. pretty simple fix to do: file: root cause
|
A profile's `set uri` may list several space-separated candidate URIs (Cobalt Strike picks one at random per request), emitted as repeated TLV_TYPE_C2_URI values. c2_verb_config now holds a char **uris / uri_count instead of a single uri; parse_c2_verb_group iterates every TLV_TYPE_C2_URI via tlv_packet_iterate_str; build_profile_url picks one at random per request (rand() % uri_count); and c2_verb_config_free frees the array. This avoids emitting the raw "uri-a uri-b" string as a single request path. GET and POST both route through build_profile_url. Also add a `docker` Makefile target that builds mettle inside the rapid7/build:mettle container (no local toolchain/autotools required), overridable via DOCKER_TARGET=<triple> and defaulting to x86_64-linux-musl. Only the mettle checkout is mounted and build artifacts are chowned back to the invoking user.
|
@dledda-r7 thank you, I will make sure this gets rolled in on the next MSF push :) |
|
Hello @OJ, is this : Handle multiple C2 URIs in mettle + add docker build target fix the issue with slack and bingsearch_getonly? |
This PR modifies Mettle so that it supports the following:
This code relies on the changes that are part of the Metasploit Framework PR. Discussion and more details can be found over there.
I'm PR'ing against
main, but should probably be part of the6.5release.