Skip to content

Commit cc0ff8f

Browse files
committed
Enable adaptive download with variable block sizes
The aim of this commit is to allow users of Meterpreter in high-latency environments have better control over the behaviour of the download function. This code contains two new options that manage the block size of the downloads and the ability to set "adaptive" which means that the block size will adjust on the fly of things continue to fail.
1 parent abeecec commit cc0ff8f

File tree

2 files changed

+40
-8
lines changed
  • lib/rex/post/meterpreter
    • extensions/stdapi/fs
    • ui/console/command_dispatcher/stdapi

2 files changed

+40
-8
lines changed

lib/rex/post/meterpreter/extensions/stdapi/fs/file.rb

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require 'rex/post/meterpreter/extensions/stdapi/fs/io'
88
require 'rex/post/meterpreter/extensions/stdapi/fs/file_stat'
99
require 'fileutils'
10+
require 'filesize'
1011

1112
module Rex
1213
module Post
@@ -25,6 +26,8 @@ class File < Rex::Post::Meterpreter::Extensions::Stdapi::Fs::IO
2526

2627
include Rex::Post::File
2728

29+
MIN_BLOCK_SIZE = 1024
30+
2831
class << self
2932
attr_accessor :client
3033
end
@@ -312,7 +315,7 @@ def File.download(dest, src_files, opts = nil, &stat)
312315
dest += timestamp
313316
end
314317

315-
stat.call('downloading', src, dest) if (stat)
318+
stat.call('Downloading', src, dest) if (stat)
316319
result = download_file(dest, src, opts, &stat)
317320
stat.call(result, src, dest) if (stat)
318321
}
@@ -325,8 +328,11 @@ def File.download_file(dest_file, src_file, opts = nil, &stat)
325328
continue=false
326329
tries=false
327330
tries_no=0
331+
stat ||= lambda { }
332+
328333
if opts
329334
continue = true if opts["continue"]
335+
adaptive = true if opts['adaptive']
330336
tries = true if opts["tries"]
331337
tries_no = opts["tries_no"]
332338
end
@@ -346,17 +352,19 @@ def File.download_file(dest_file, src_file, opts = nil, &stat)
346352
dir = ::File.dirname(dest_file)
347353
::FileUtils.mkdir_p(dir) if dir and not ::File.directory?(dir)
348354

355+
src_size = Filesize.new(src_stat.size).pretty
356+
349357
if continue
350358
# continue downloading the file - skip downloaded part in the source
351359
dst_fd = ::File.new(dest_file, "ab")
352360
begin
353361
dst_fd.seek(0, ::IO::SEEK_END)
354362
in_pos = dst_fd.pos
355363
src_fd.seek(in_pos)
356-
stat.call('continuing from ', in_pos, src_file) if (stat)
364+
stat.call("Continuing from #{Filesize.new(in_pos).pretty} of #{src_size}", src_file, dest_file)
357365
rescue
358366
# if we can't seek, download again
359-
stat.call('error continuing - downloading from scratch', src_file, dest_file) if (stat)
367+
stat.call('Error continuing - downloading from scratch', src_file, dest_file)
360368
dst_fd.close
361369
dst_fd = ::File.new(dest_file, "wb")
362370
end
@@ -365,41 +373,59 @@ def File.download_file(dest_file, src_file, opts = nil, &stat)
365373
end
366374

367375
# Keep transferring until EOF is reached...
376+
block_size = opts['block_size'] || 1024 * 1024
368377
begin
369378
if tries
370379
# resume when timeouts encountered
371380
seek_back = false
381+
adjust_block = false
372382
tries_cnt = 0
373383
begin # while
374384
begin # exception
375385
if seek_back
376386
in_pos = dst_fd.pos
377387
src_fd.seek(in_pos)
378388
seek_back = false
379-
stat.call('resuming at ', in_pos, src_file) if (stat)
389+
stat.call("Resuming at #{Filesize.new(in_pos).pretty} of #{src_size}", src_file, dest_file)
380390
else
381391
# succesfully read and wrote - reset the counter
382392
tries_cnt = 0
383393
end
384-
data = src_fd.read
394+
adjust_block = true
395+
data = src_fd.read(block_size)
396+
adjust_block = false
385397
rescue Rex::TimeoutError
386398
# timeout encountered - either seek back and retry or quit
387399
if (tries && (tries_no == 0 || tries_cnt < tries_no))
388400
tries_cnt += 1
389401
seek_back = true
390-
stat.call('error downloading - retry #', tries_cnt, src_file) if (stat)
402+
# try a smaller block size for the next round
403+
if adaptive && adjust_block
404+
block_size = [block_size >> 1, MIN_BLOCK_SIZE].max
405+
adjust_block = false
406+
msg = "Error downloading, block size set to #{block_size} - retry # #{tries_cnt}"
407+
stat.call(msg, src_file, dest_file)
408+
else
409+
stat.call("Error downloading - retry # #{tries_cnt}", src_file, dest_file)
410+
end
391411
retry
392412
else
393-
stat.call('error downloading - giving up', src_file, dest_file) if (stat)
413+
stat.call('Error downloading - giving up', src_file, dest_file)
394414
raise
395415
end
396416
end
397417
dst_fd.write(data) if (data != nil)
418+
percent = dst_fd.pos.to_f / src_stat.size.to_f * 100.0
419+
msg = "Downloaded #{Filesize.new(dst_fd.pos).pretty} of #{src_size} (#{percent.round(2)}%)"
420+
stat.call(msg, src_file, dest_file)
398421
end while (data != nil)
399422
else
400423
# do the simple copying quiting on the first error
401-
while ((data = src_fd.read) != nil)
424+
while ((data = src_fd.read(block_size)) != nil)
402425
dst_fd.write(data)
426+
percent = dst_fd.pos.to_f / src_stat.size.to_f * 100.0
427+
msg = "Downloaded #{Filesize.new(dst_fd.pos).pretty} of #{src_size} (#{percent.round(2)}%)"
428+
stat.call(msg, src_file, dest_file)
403429
end
404430
end
405431
rescue EOFError

lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ class Console::CommandDispatcher::Stdapi::Fs
2828
@@download_opts = Rex::Parser::Arguments.new(
2929
"-h" => [ false, "Help banner." ],
3030
"-c" => [ false, "Resume getting a partially-downloaded file." ],
31+
"-a" => [ false, "Enable adaptive download buffer size." ],
32+
"-b" => [ true, "Set the initial block size for the download." ],
3133
"-l" => [ true, "Set the limit of retries (0 unlimits)." ],
3234
"-r" => [ false, "Download recursively." ],
3335
"-t" => [ false, "Timestamp downloaded files." ])
@@ -382,6 +384,10 @@ def cmd_download(*args)
382384

383385
@@download_opts.parse(args) { |opt, idx, val|
384386
case opt
387+
when "-a"
388+
opts['adaptive'] = true
389+
when "-b"
390+
opts['block_size'] = val.to_i
385391
when "-r"
386392
recursive = true
387393
opts['recursive'] = true

0 commit comments

Comments
 (0)