Add upload/download/delete/mkdir/rmdir to smb session

This commit is contained in:
Dean Welch 2024-02-27 17:52:19 +00:00
parent d964edde11
commit 689caf4bd1
13 changed files with 429 additions and 92 deletions

View File

@ -81,7 +81,7 @@ PATH
rex-zip
ruby-macho
ruby-mysql
ruby_smb (~> 3.3.0)
ruby_smb (~> 3.3.3)
rubyntlm
rubyzip
sinatra
@ -474,8 +474,8 @@ GEM
ruby-progressbar (1.13.0)
ruby-rc4 (0.1.5)
ruby2_keywords (0.0.5)
ruby_smb (3.3.2)
bindata
ruby_smb (3.3.3)
bindata (= 2.4.15)
openssl-ccm
openssl-cmac
rubyntlm

View File

@ -13,6 +13,8 @@ class Msf::Sessions::SMB
attr_accessor :console
# @return [RubySMB::Client] The SMB client
attr_accessor :client
# @return [Rex::Proto::SMB::SimpleClient]
attr_accessor :simple_client
attr_accessor :platform, :arch
attr_reader :framework
@ -21,6 +23,7 @@ class Msf::Sessions::SMB
# @option opts [RubySMB::Client] :client
def initialize(rstream, opts = {})
@client = opts.fetch(:client)
@simple_client = ::Rex::Proto::SMB::SimpleClient.new(client.dispatcher.tcp_socket, client: client)
self.console = Rex::Post::SMB::Ui::Console.new(self)
super(rstream, opts)
end

15
lib/rex/ntpath.rb Normal file
View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Rex
module Ntpath
# @param [String] path The path to convert into a valid ntpath format
def self.as_ntpath(path)
Pathname.new(path)
.cleanpath
.each_filename
.drop_while { |file| file == '.' }
.join('\\')
end
end
end

View File

@ -36,6 +36,7 @@ module Rex
# The ruby smb client context
self.session = session
self.client = session.client
self.simple_client = session.simple_client
# Queued commands array
self.commands = []
@ -125,6 +126,9 @@ module Rex
# @return [RubySMB::Client]
attr_reader :client # :nodoc:
# @return [Rex::Proto::SMB::SimpleClient]
attr_reader :simple_client
# @return [RubySMB::SMB2::Tree]
attr_accessor :active_share
@ -134,7 +138,7 @@ module Rex
def format_prompt(val)
if active_share
share_name = active_share.share[/[^\\].*$/, 0]
cwd = self.cwd.blank? ? '' : "\\#{self.cwd}"
cwd = self.cwd.blank? ? '' : "\\#{Rex::Ntpath.as_ntpath(self.cwd)}"
prompt = "#{share_name}#{cwd}"
else
prompt = session.address.to_s
@ -145,9 +149,8 @@ module Rex
protected
attr_writer :session, :client # :nodoc: # :nodoc:
attr_writer :session, :client, :simple_client # :nodoc: # :nodoc:
attr_accessor :commands # :nodoc:
end
end
end

View File

@ -36,6 +36,14 @@ module Rex
console.client
end
#
# Returns the smb simple client.
#
# @return [Rex::Proto::SMB::SimpleClient]
def simple_client
shell.simple_client
end
#
# Returns the smb session context.
#

View File

@ -1,6 +1,8 @@
# -*- coding: binary -*-
require 'pathname'
require 'rex/post/file'
require 'filesize'
module Rex
module Post
@ -16,7 +18,7 @@ module Rex
include Rex::Post::SMB::Ui::Console::CommandDispatcher
#
# Initializes an instance of the core command set using the supplied console
# Initializes an instance of the shares command set using the supplied console
# for interactivity.
#
# @param [Rex::Post::SMB::Ui::Console] console
@ -48,6 +50,26 @@ module Rex
['-h', '--help'] => [false, 'Help menu' ]
)
@@upload_opts = Rex::Parser::Arguments.new(
['-h', '--help'] => [false, 'Help menu' ]
)
@@download_opts = Rex::Parser::Arguments.new(
['-h', '--help'] => [false, 'Help menu' ]
)
@@delete_opts = Rex::Parser::Arguments.new(
['-h', '--help'] => [false, 'Help menu' ]
)
@@mkdir_opts = Rex::Parser::Arguments.new(
['-h', '--help'] => [false, 'Help menu' ]
)
@@rmdir_opts = Rex::Parser::Arguments.new(
['-h', '--help'] => [false, 'Help menu' ]
)
#
# List of supported commands.
#
@ -58,7 +80,12 @@ module Rex
'dir' => 'List all files in the current directory (alias for ls)',
'pwd' => 'Print the current remote working directory',
'cd' => 'Change the current remote working directory',
'cat' => 'Read the file at the given path'
'cat' => 'Read the file at the given path',
'upload' => 'Upload a file',
'download' => 'Download a file',
'delete' => 'Delete a file',
'mkdir' => 'Make a new directory',
'rmdir' => 'Delete a directory'
}
reqs = {}
@ -157,9 +184,24 @@ module Rex
return print_no_share_selected unless active_share
files = active_share.list(directory: as_ntpath(shell.cwd))
remote_path = ''
@@delete_opts.parse(args) do |_opt, idx, val|
case idx
when 0
remote_path = val
else
print_warning('Too many parameters')
cmd_ls_help
return
end
end
full_path = Rex::Ntpath.as_ntpath(Pathname.new(shell.cwd).join(remote_path).to_s)
files = active_share.list(directory: full_path)
table = Rex::Text::Table.new(
'Header' => 'Shares',
'Header' => "ls #{full_path}",
'Indent' => 4,
'Columns' => [ '#', 'Type', 'Name', 'Created', 'Accessed', 'Written', 'Changed', 'Size'],
'Rows' => files.map.with_index do |file, i|
@ -255,12 +297,11 @@ module Rex
return print_no_share_selected unless active_share
path = args[0]
# TODO: Needs better normalization
new_path = as_ntpath(Pathname.new(shell.cwd).join(path).to_s)
native_path = Pathname.new(shell.cwd).join(path).to_s
new_path = Rex::Ntpath.as_ntpath(native_path)
begin
response = active_share.open_directory(directory: new_path)
directory = RubySMB::SMB2::File.new(name: new_path, tree: active_share, response: response, encrypt: @tree_connect_encrypt_data)
directory.close
rescue RubySMB::Error::UnexpectedStatusCode => e
# Special case this error to provide better feedback to the user
# since I think trying to `cd` to a non-existent directory is pretty likely to accidentally happen
@ -274,9 +315,11 @@ module Rex
print_error('Unknown error occurred while trying to change directory')
elog(e)
return
ensure
directory.close if directory
end
shell.cwd = new_path
shell.cwd = native_path
end
def cmd_cat_help
@ -295,14 +338,14 @@ module Rex
return
end
return print_no_share_selected if !active_share
return print_no_share_selected unless active_share
path = args[0]
new_path = as_ntpath(Pathname.new(shell.cwd).join(path).to_s)
new_path = Rex::Ntpath.as_ntpath(Pathname.new(shell.cwd).join(path).to_s)
begin
file = active_share.open_file(filename: new_path)
file = simple_client.open(new_path, 'o')
result = file.read
print_line(result)
rescue StandardError => e
@ -320,7 +363,209 @@ module Rex
def cmd_cd_tabs(_str, words)
return [] if words.length > 1
@@cat_opts.option_keys
@@cd_opts.option_keys
end
def cmd_upload(*args)
if args.include?('-h') || args.include?('--help')
cmd_upload_help
return
end
return print_no_share_selected unless active_share
local_path = nil
remote_path = nil
@@upload_opts.parse(args) do |_opt, idx, val|
case idx
when 0
local_path = val
when 1
remote_path = val
else
print_warning('Too many parameters')
cmd_upload_help
return
end
end
if local_path.blank?
print_error('No local path given')
return
end
remote_path = Rex::Post::File.basename(local_path) if remote_path.nil?
full_path = Rex::Ntpath.as_ntpath(Pathname.new(shell.cwd).join(remote_path).to_s)
upload_file(full_path, local_path)
print_good("#{local_path} uploaded to #{full_path}")
end
def cmd_upload_tabs(str, words)
tab_complete_filenames(str, words)
end
def cmd_upload_help
print_line 'Usage: upload <local_path> <remote_path>'
print_line
print_line 'Upload a file to the remote target.'
print @@upload_opts.usage
end
def cmd_download(*args)
if args.include?('-h') || args.include?('--help')
cmd_download_help
return
end
return print_no_share_selected unless active_share
remote_path = nil
local_path = nil
@@download_opts.parse(args) do |_opt, idx, val|
case idx
when 0
remote_path = val
when 1
local_path = val
else
print_warning('Too many parameters')
cmd_download_help
return
end
end
if remote_path.blank?
print_error('No remote path given')
return
end
local_path = Rex::Post::File.basename(remote_path) if local_path.nil?
full_path = Rex::Ntpath.as_ntpath(Pathname.new(shell.cwd).join(remote_path).to_s)
download_file(local_path, full_path)
print_good("Downloaded #{full_path} to #{local_path}")
end
def cmd_download_help
print_line 'Usage: download <remote_path> <local_path>'
print_line
print_line 'Download a file from the remote target.'
print @@download_opts.usage
end
def cmd_delete(*args)
if args.include?('-h') || args.include?('--help')
cmd_delete_help
return
end
remote_path = nil
@@delete_opts.parse(args) do |_opt, idx, val|
case idx
when 0
remote_path = val
else
print_warning('Too many parameters')
cmd_delete_help
return
end
end
full_path = Rex::Ntpath.as_ntpath(Pathname.new(shell.cwd).join(remote_path).to_s)
fd = simple_client.open(full_path, 'o')
fd.delete
print_good("Deleted #{full_path}")
end
def cmd_delete_help
print_line 'Usage: delete <remote_path>'
print_line
print_line 'Delete a file from the remote target.'
print @@delete_opts.usage
end
def cmd_mkdir(*args)
if args.include?('-h') || args.include?('--help')
cmd_mkdir_help
return
end
return print_no_share_selected unless active_share
remote_path = nil
@@mkdir_opts.parse(args) do |_opt, idx, val|
case idx
when 0
remote_path = val
else
print_warning('Too many parameters')
cmd_mkdir_help
return
end
end
full_path = Rex::Ntpath.as_ntpath(Pathname.new(shell.cwd).join(remote_path).to_s)
response = active_share.open_directory(directory: full_path, disposition: RubySMB::Dispositions::FILE_CREATE)
directory = RubySMB::SMB2::File.new(name: full_path, tree: active_share, response: response, encrypt: @tree_connect_encrypt_data)
print_good("Directory #{full_path} created")
ensure
directory.close if directory
end
def cmd_mkdir_help
print_line 'Usage: mkdir <remote_path>'
print_line
print_line 'Create a directory on the remote target.'
print @@mkdir_opts.usage
end
def cmd_rmdir(*args)
if args.include?('-h') || args.include?('--help')
cmd_rmdir_help
return
end
return print_no_share_selected unless active_share
remote_path = nil
@@rmdir_opts.parse(args) do |_opt, idx, val|
case idx
when 0
remote_path = val
else
print_warning('Too many parameters')
cmd_rmdir_help
return
end
end
full_path = Rex::Ntpath.as_ntpath(Pathname.new(shell.cwd).join(remote_path).to_s)
response = active_share.open_directory(directory: full_path, write: true, delete: true, desired_delete: true)
directory = RubySMB::SMB2::File.new(name: full_path, tree: active_share, response: response, encrypt: @tree_connect_encrypt_data)
status = directory.delete
if status == WindowsError::NTStatus::STATUS_SUCCESS
print_good("Deleted #{full_path}")
else
print_error("Error deleting #{full_path}: #{status.name}, #{status.description}")
end
ensure
directory.close if directory
end
def cmd_rmdir_help
print_line 'Usage: rmdir <remote_path>'
print_line
print_line 'Delete a directory from the remote target.'
print @@rmdir_opts.usage
end
protected
@ -330,12 +575,56 @@ module Rex
nil
end
def as_ntpath(path)
Pathname.new(path)
.cleanpath
.each_filename
.drop_while { |file| file == '.' || file == '..' }
.join('\\')
# Upload a local file to the target
# @param dest_file [String] The path for the destination file
# @param src_file [String] The path for the source file
def upload_file(dest_file, src_file)
buf_size = 8 * 1024 * 1024
begin
dest_fd = simple_client.open(dest_file, 'wct', write: true)
src_fd = ::File.open(src_file, "rb")
src_size = src_fd.stat.size
offset = 0
while (buf = src_fd.read(buf_size))
offset = dest_fd.write(buf, offset)
percent = offset / src_size.to_f * 100.0
msg = "Uploaded #{Filesize.new(offset).pretty} of " \
"#{Filesize.new(src_size).pretty} (#{percent.round(2)}%)"
print_status(msg)
end
ensure
src_fd.close unless src_fd.nil?
dest_fd.close unless dest_fd.nil?
end
end
# Download a remote file from the target
# @param dest_file [String] The path for the destination file
# @param src_file [String] The path for the source file
def download_file(dest_file, src_file)
buf_size = 8 * 1024 * 1024
src_fd = simple_client.open(src_file, 'o')
# Make the destination path if necessary
dir = ::File.dirname(dest_file)
::FileUtils.mkdir_p(dir) if dir && !::File.directory?(dir)
dst_fd = ::File.new(dest_file, "wb")
offset = 0
src_size = client.open_files[src_fd.file_id].size
begin
while offset < src_size
data = src_fd.read(buf_size, offset)
dst_fd.write(data)
offset += data.length
percent = offset / src_size.to_f * 100.0
msg = "Downloaded #{Filesize.new(offset).pretty} of " \
"#{Filesize.new(src_size).pretty} (#{percent.round(2)}%)"
print_status(msg)
end
ensure
src_fd.close unless src_fd.nil?
dst_fd.close unless dst_fd.nil?
end
end
end
end

View File

@ -214,6 +214,7 @@ class SimpleClient
end
file_id = self.client.open(path, mode, read: true, write: write || perm.include?('w'))
else
mode = UTILS.open_mode_to_mode(perm)
access = UTILS.open_mode_to_access(perm)

View File

@ -30,35 +30,36 @@ module Rex::Proto::SMB
end
def read_ruby_smb(length, offset, depth = 0)
file_size = client.open_files[client.last_file_id].size
file_size_remaining = file_size - offset
if length.nil?
max_size = client.open_files[client.last_file_id].size
fptr = offset
chunk = [max_size, chunk_size].min
data = client.read(file_id, fptr, chunk).pack('C*')
fptr = data.length
while data.length < max_size
if (max_size - data.length) < chunk
chunk = max_size - data.length
end
data << client.read(file_id, fptr, chunk).pack('C*')
fptr = data.length
end
max_size = file_size_remaining
else
begin
data = client.read(file_id, offset, length).pack('C*')
rescue RubySMB::Error::UnexpectedStatusCode => e
if e.message == 'STATUS_PIPE_EMPTY' && depth < 20
data = read_ruby_smb(length, offset, depth + 1)
else
raise e
end
max_size = [length, file_size_remaining].min
end
fptr = offset
chunk = [max_size, chunk_size].min
data = client.read(file_id, fptr, chunk).pack('C*')
fptr += data.length
while data.length < max_size
if (max_size - data.length) < chunk
chunk = max_size - data.length
end
new_data = client.read(file_id, fptr, chunk).pack('C*')
data << new_data
fptr += new_data.length
end
data
rescue RubySMB::Error::UnexpectedStatusCode => e
if e.message == 'STATUS_PIPE_EMPTY' && depth < 20
read_ruby_smb(max_size, offset, depth + 1)
else
raise e
end
end
def read_rex_smb(length, offset)
@ -139,6 +140,7 @@ module Rex::Proto::SMB
fptr += cl
chunk = data.slice!(0, chunk_size)
end
fptr
end
end
end

View File

@ -21,6 +21,36 @@ class OpenPipe < OpenFile
@buff.slice!(0, length)
end
def read_ruby_smb(length, offset, depth = 0)
if length.nil?
max_size = client.open_files[client.last_file_id].size
fptr = offset
chunk = [max_size, chunk_size].min
data = client.read(file_id, fptr, chunk).pack('C*')
fptr = data.length
while data.length < max_size
if (max_size - data.length) < chunk
chunk = max_size - data.length
end
data << client.read(file_id, fptr, chunk).pack('C*')
fptr = data.length
end
else
begin
client.read(file_id, offset, length).pack('C*')
rescue RubySMB::Error::UnexpectedStatusCode => e
if e.message == 'STATUS_PIPE_EMPTY' && depth < 20
read_ruby_smb(length, offset, depth + 1)
else
raise e
end
end
end
end
def read(length = nil, offset = 0)
case self.mode
when 'trans'

View File

@ -147,7 +147,7 @@ Gem::Specification.new do |spec|
spec.add_runtime_dependency 'net-ssh'
spec.add_runtime_dependency 'ed25519' # Adds ed25519 keys for net-ssh
spec.add_runtime_dependency 'bcrypt_pbkdf'
spec.add_runtime_dependency 'ruby_smb', '~> 3.3.0'
spec.add_runtime_dependency 'ruby_smb', '~> 3.3.3'
spec.add_runtime_dependency 'net-imap' # Used in Postgres auth for its SASL stringprep implementation
spec.add_runtime_dependency 'net-ldap'
spec.add_runtime_dependency 'net-smtp'

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
require 'rspec'
RSpec.describe Rex::Ntpath do
describe '#as_ntpath' do
let(:valid_windows_path) { 'some\\path\\that\\is\\valid' }
[
'some\\path\\that\\is\\valid',
'some/path/that/is/valid',
'some/./path/that/./is/valid',
'some/extra/../path/that/extra/../is/valid',
'/some/path/that/is/valid'
].each do |path|
context "when the path is #{path}" do
it 'formats it as a valid ntpath' do
formatted_path = described_class.as_ntpath(path)
expect(formatted_path).to eq valid_windows_path
end
end
end
end
end

View File

@ -4,7 +4,10 @@ require 'spec_helper'
require 'rex/post/smb/ui/console/command_dispatcher/core'
RSpec.describe Rex::Post::SMB::Ui::Console::CommandDispatcher::Core do
let(:client) { instance_double(RubySMB::Client) }
let(:client) { instance_double(RubySMB::Client, dispatcher: dispatcher) }
let(:simple_client) { instance_double(Rex::Proto::SMB::SimpleClient) }
let(:dispatcher) { instance_double(RubySMB::Dispatcher::Socket, tcp_socket: socket) }
let(:socket) { instance_double(IO) }
let(:session) { Msf::Sessions::SMB.new(nil, { client: client }) }
let(:console) do
console = Rex::Post::SMB::Ui::Console.new(session)
@ -13,6 +16,7 @@ RSpec.describe Rex::Post::SMB::Ui::Console::CommandDispatcher::Core do
end
before(:each) do
allow(Rex::Proto::SMB::SimpleClient).to receive(:new).and_return(simple_client)
allow(session).to receive(:client).and_return(client)
allow(session).to receive(:console).and_return(console)
allow(session).to receive(:name).and_return('test client name')

View File

@ -1,43 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
require 'rex/post/smb/ui/console'
require 'rex/post/smb/ui/console/command_dispatcher/shares'
RSpec.describe Rex::Post::SMB::Ui::Console::CommandDispatcher::Shares do
let(:client) { instance_double(RubySMB::Client) }
let(:session) { Msf::Sessions::SMB.new(nil, { client: client }) }
let(:console) do
console = Rex::Post::SMB::Ui::Console.new(session)
console.disable_output = true
console
end
before(:each) do
allow(session).to receive(:client).and_return(client)
allow(session).to receive(:console).and_return(console)
allow(session).to receive(:name).and_return('test client name')
allow(session).to receive(:sid).and_return('test client sid')
end
subject(:command_dispatcher) { described_class.new(session.console) }
describe '#as_ntpath' do
let(:valid_windows_path) { 'some\\path\\that\\is\\valid' }
[
'some\\path\\that\\is\\valid',
'some/path/that/is/valid',
'some/./path/that/./is/valid',
'some/extra/../path/that/extra/../is/valid',
'/some/path/that/is/valid'
].each do |path|
context "when the path is #{path}" do
it 'formats it as a valid ntpath' do
formatted_path = subject.send(:as_ntpath, path)
expect(formatted_path).to eq valid_windows_path
end
end
end
end
end