Land #17385, Return success code for writing and appending file in command shells

This commit is contained in:
bwatters 2023-01-17 16:37:06 -06:00
commit 470972b91d
No known key found for this signature in database
GPG Key ID: ECC0F0A52E65F268
3 changed files with 95 additions and 61 deletions

View File

@ -406,13 +406,13 @@ Shell Banner:
print_line("Usage: download [src] [dst]")
print_line
print_line("Downloads remote files to the local machine.")
print_line("This command does not support directories")
print_line("This command does not support to download a FOLDER yet")
print_line
end
def cmd_download(*args)
if args.length != 2
# no arguments, just print help message
# no argumnets, just print help message
return cmd_download_help
end
@ -421,72 +421,53 @@ Shell Banner:
# Check if src exists
if !_file_transfer.file_exist?(src)
print_error('The target file does not exist')
print_error("The target file does not exist")
return
end
fs_sep = platform == 'windows' ? '\\' : '/'
if dst.blank?
dst = src.split(fs_sep).last
elsif ::File.directory?(dst)
dst += ::File::SEPARATOR unless dst.end_with?(::File::SEPARATOR)
dst += src.split(fs_sep).last
end
dst_dir = ::File.dirname(dst)
::FileUtils.mkdir_p(dst_dir) if dst_dir and not ::File.directory?(dst_dir)
# Get file content
# match the output style of the Meterpreter equivalent
print_status("Downloading: #{src} -> #{dst}")
print_status("Download #{src} => #{dst}")
content = _file_transfer.read_file(src)
# Write file to local machine
::File.binwrite(dst, content)
print_status("Completed : #{src} -> #{dst}")
File.binwrite(dst, content)
print_good("Done")
end
def cmd_upload_help
print_line("Usage: upload [src] [dst]")
print_line
print_line("Uploads load file to the victim machine.")
print_line("This command does not support directories")
print_line("This command does not support to upload a FOLDER yet")
print_line
end
def cmd_upload(*args)
if args.length != 2
# no arguments, just print help message
# no argumnets, just print help message
return cmd_upload_help
end
src = args[0]
dst = args[1]
if dst.blank?
dst = ::File.basename(src)
elsif _file_transfer.directory?(dst)
fs_sep = platform == 'windows' ? '\\' : '/'
dst += fs_sep unless dst.end_with?(fs_sep)
dst += ::File.basename(src)
end
# Check target file exists on the target machine
if _file_transfer.file_exist?(dst)
print_warning('The target file already exists')
unless prompt_yesno("Overwrite the target file #{dst}?")
print_warning("The file <#{dst}> already exists on the target machine")
unless prompt_yesno("Overwrite the target file <#{dst}>?")
return
end
end
print_status("Uploading : #{src} -> #{dst}")
begin
# Read file from local machine
content = ::File.binread(src)
_file_transfer.write_file(dst, content)
print_status("Completed : #{src} -> #{dst}")
content = File.binread(src)
result = _file_transfer.write_file(dst, content)
print_good("File <#{dst}> upload finished") if result
print_error("Error occured while uploading <#{src}> to <#{dst}>") unless result
rescue => e
print_error("Failed : #{src} -> #{dst} - #{e.message}")
print_error("Error occured while uploading <#{src}> to <#{dst}> - #{e.message}")
elog(e)
return
end
end

View File

@ -59,7 +59,7 @@ class Msf::Sessions::PowerShell < Msf::Sessions::CommandShell
etime = ::Time.now.to_f + timeout
buff = ''
# Keep reading data until the marker has been received or the 30 minture timeout has occured
# Keep reading data until the marker has been received or the 30 minute timeout has occured
while (::Time.now.to_f < etime)
res = shell_read(-1, timeout)
break unless res

View File

@ -504,21 +504,20 @@ module Msf::Post::File
if session.type == 'meterpreter'
return _write_file_meterpreter(file_name, data)
elsif session.type == 'powershell'
_write_file_powershell(file_name, data)
return _write_file_powershell(file_name, data)
elsif session.respond_to? :shell_command_token
if session.platform == 'windows'
if _can_echo?(data)
_win_ansi_write_file(file_name, data)
return _win_ansi_write_file(file_name, data)
else
_win_bin_write_file(file_name, data)
return _win_bin_write_file(file_name, data)
end
else
_write_file_unix_shell(file_name, data)
return _write_file_unix_shell(file_name, data)
end
else
return false
end
true
end
#
@ -531,7 +530,7 @@ module Msf::Post::File
if session.type == 'meterpreter'
return _write_file_meterpreter(file_name, data, 'ab')
elsif session.type == 'powershell'
_append_file_powershell(file_name, data)
return _append_file_powershell(file_name, data)
elsif session.respond_to? :shell_command_token
if session.platform == 'windows'
if _can_echo?(data)
@ -543,7 +542,6 @@ module Msf::Post::File
return _append_file_unix_shell(file_name, data)
end
end
true
end
#
@ -712,25 +710,38 @@ module Msf::Post::File
def _write_file_powershell(file_name, data, append = false)
offset = 0
chunk_size = 16256
chunk_size = 1000
loop do
_write_file_powershell_fragment(file_name, data, offset, chunk_size, append)
offset += chunk_size + 1
success = _write_file_powershell_fragment(file_name, data, offset, chunk_size, append)
unless success
unless offset == 0
print_warning("Write partially succeeded then failed. May need to manually clean up #{file_name}")
end
return false
end
# Future writes will then append, regardless of whether this is an append or write operation
append = true
offset += chunk_size
break if offset >= data.length
end
true
end
def _write_file_powershell_fragment(file_name, data, offset, chunk_size, append = false)
chunk = data[offset..offset + chunk_size]
token = "_#{::Rex::Text.rand_text_alpha(32)}"
chunk = data[offset..(offset + chunk_size-1)]
length = chunk.length
compressed_chunk = Rex::Text.gzip(chunk)
encoded_chunk = Base64.strict_encode64(compressed_chunk)
if offset > 0 || append
if append
file_mode = 'Append'
else
file_mode = 'Create'
end
pwsh_code = <<~PSH
try {
$encoded='#{encoded_chunk}';
$gzip_bytes=[System.Convert]::FromBase64String($encoded);
$mstream = New-Object System.IO.MemoryStream(,$gzip_bytes);
@ -741,8 +752,14 @@ module Msf::Post::File
$filestream.Write($file_bytes,0,$file_bytes.Length);
$filestream.Close();
$gzipstream.Close();
echo Done
} catch {
echo #{token}
}
PSH
cmd_exec(pwsh_code)
result = cmd_exec(pwsh_code)
return result.include?(length.to_s) && !result.include?(token) && result.include?('Done')
end
def _read_file_powershell(filename)
@ -839,11 +856,14 @@ protected
def _win_ansi_write_file(file_name, data, chunk_size = 5000)
start_index = 0
write_length = [chunk_size, data.length].min
session.shell_command_token("echo | set /p=\"#{data[0, write_length]}\"> \"#{file_name}\"")
success = _shell_command_with_success_code("echo | set /p x=\"#{data[0, write_length]}\"> \"#{file_name}\"")
return false unless success
if data.length > write_length
# just use append to finish the rest
_win_ansi_append_file(file_name, data[write_length, data.length], chunk_size)
return _win_ansi_append_file(file_name, data[write_length, data.length], chunk_size)
end
true
end
# Windows ansi file append for shell sessions. Writes given object content to a remote file.
@ -859,14 +879,22 @@ protected
write_length = [chunk_size, data.length].min
while start_index < data.length
begin
session.shell_command_token("<nul set /p=\"#{data[start_index, write_length]}\" >> \"#{file_name}\"")
success = _shell_command_with_success_code("echo | set /p x=\"#{data[start_index, write_length]}\">> \"#{file_name}\"")
unless success
print_warning("Write partially succeeded then failed. May need to manually clean up #{file_name}") unless start_index == 0
return false
end
start_index += write_length
write_length = [chunk_size, data.length - start_index].min
rescue ::Exception => e
print_error("Exception while running #{__method__}: #{e}")
print_warning("May need to manually clean up #{file_name}") unless start_index == 0
file_rm(file_name)
return false
end
end
true
end
# Windows binary file write for shell sessions. Writes given object content to a remote file.
@ -879,13 +907,19 @@ protected
b64_data = Base64.strict_encode64(data)
b64_filename = "#{file_name}.b64"
begin
_win_ansi_write_file(b64_filename, b64_data, chunk_size)
cmd_exec("certutil -f -decode #{b64_filename} #{file_name}")
success = _win_ansi_write_file(b64_filename, b64_data, chunk_size)
return false unless success
vprint_status("Uploaded Base64-encoded file. Decoding using certutil")
success = _shell_command_with_success_code("certutil -f -decode #{b64_filename} #{file_name}")
return false unless success
rescue ::Exception => e
print_error("Exception while running #{__method__}: #{e}")
return false
ensure
file_rm(b64_filename)
end
true
end
# Windows binary file append for shell sessions. Appends given object content to a remote file.
@ -899,15 +933,23 @@ protected
b64_filename = "#{file_name}.b64"
tmp_filename = "#{file_name}.tmp"
begin
_win_ansi_write_file(b64_filename, b64_data, chunk_size)
cmd_exec("certutil -decode #{b64_filename} #{tmp_filename}")
cmd_exec("copy /b #{file_name}+#{tmp_filename} #{file_name}")
success = _win_ansi_write_file(b64_filename, b64_data, chunk_size)
return false unless success
vprint_status("Uploaded Base64-encoded file. Decoding using certutil")
success = _shell_command_with_success_code("certutil -decode #{b64_filename} #{tmp_filename}")
return false unless success
vprint_status("Certutil succeeded. Appending using copy")
success = _shell_command_with_success_code("copy /b #{file_name}+#{tmp_filename} #{file_name}")
return false unless success
rescue ::Exception => e
print_error("Exception while running #{__method__}: #{e}")
return false
ensure
file_rm(b64_filename)
file_rm(tmp_filename)
end
true
end
#
@ -937,8 +979,7 @@ protected
# Short-circuit an empty string. The : builtin is part of posix
# standard and should theoretically exist everywhere.
if data.empty?
session.shell_command_token(": #{redirect} #{file_name}")
return
return _shell_command_with_success_code(": #{redirect} #{file_name}")
end
d = data.dup
@ -1039,7 +1080,8 @@ protected
# The first command needs to use the provided redirection for either
# appending or truncating.
cmd = command.sub('CONTENTS') { chunks.shift }
session.shell_command_token("#{cmd} #{redirect} \"#{file_name}\"")
succeeded = _shell_command_with_success_code("#{cmd} #{redirect} \"#{file_name}\"")
return false unless succeeded
# After creating/truncating or appending with the first command, we
# need to append from here on out.
@ -1047,12 +1089,23 @@ protected
vprint_status("Next chunk is #{chunk.length} bytes")
cmd = command.sub('CONTENTS') { chunk }
session.shell_command_token("#{cmd} >> '#{file_name}'")
succeeded = _shell_command_with_success_code("#{cmd} >> '#{file_name}'")
unless succeeded
print_warning("Write partially succeeded then failed. May need to manually clean up #{file_name}")
return false
end
end
true
end
def _shell_command_with_success_code(cmd)
token = "_#{::Rex::Text.rand_text_alpha(32)}"
result = session.shell_command_token("#{cmd} && echo #{token}")
return result.include?(token)
end
#
# Calculate the maximum line length for a unix shell.
#