Merge pull request #21232 from bcoles/file-find_writable_directories
Add find_writable_directories to Msf::Post::File
This commit is contained in:
@@ -702,6 +702,52 @@ module Msf::Post::File
|
||||
end
|
||||
alias cp_file copy_file
|
||||
|
||||
#
|
||||
# Find writable directories under +path+ on a Unix system.
|
||||
#
|
||||
# Uses find's +-writable+ flag which checks effective access for the current user.
|
||||
#
|
||||
# @param path [String] Absolute base path to search from (default: '/')
|
||||
# @param max_depth [Integer] Maximum directory depth to search (0 = base directory only)
|
||||
# @param timeout [Integer] Maximum seconds for cmd_exec to wait (default: 15).
|
||||
# Note: if the command times out, the remote find process may continue
|
||||
# running and tie up the shell channel until it finishes.
|
||||
# @return [Array<String>, nil] Array of writable directory paths, or nil on failure
|
||||
#
|
||||
def find_writable_directories(path: '/', max_depth: 2, timeout: 15)
|
||||
raise "`find_writable_directories' method does not support Windows systems" if session.platform == 'windows'
|
||||
|
||||
path = path.to_s
|
||||
raise ArgumentError, 'path must be an absolute path' unless path.start_with?('/')
|
||||
|
||||
max_depth = max_depth.to_i
|
||||
raise ArgumentError, 'max_depth must not be negative' if max_depth < 0
|
||||
|
||||
timeout = timeout.to_i
|
||||
|
||||
if max_depth > 2
|
||||
print_warning("Large max_depth (#{max_depth}) may cause the find command to run for a long time and hang the session")
|
||||
end
|
||||
|
||||
escaped_path = session.escape_arg(path)
|
||||
find_args = ["find #{escaped_path}"]
|
||||
find_args << "-maxdepth #{max_depth}"
|
||||
find_args << '-type d'
|
||||
find_args << '-writable'
|
||||
|
||||
find_args << '2>/dev/null'
|
||||
cmd = find_args.join(' ')
|
||||
exec_timeout = timeout > 0 ? timeout : 15
|
||||
|
||||
begin
|
||||
cmd_exec(cmd, nil, exec_timeout).to_s.lines.map(&:strip).select { |p| p.start_with?('/') }
|
||||
rescue ::StandardError => e
|
||||
elog("Failed to find writable directories in #{path}", error: e)
|
||||
print_error("Failed to find writable directories in #{path}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def _append_file_powershell(file_name, data)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'rspec'
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Msf::Post::File do
|
||||
subject do
|
||||
@@ -63,4 +63,103 @@ RSpec.describe Msf::Post::File do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#find_writable_directories' do
|
||||
subject do
|
||||
described_mixin = described_class
|
||||
klass = Class.new do
|
||||
include described_mixin
|
||||
attr_accessor :session
|
||||
def cmd_exec(*_args); ''; end
|
||||
def print_warning(_msg); end
|
||||
def print_error(_msg); end
|
||||
def elog(*_args); end
|
||||
end
|
||||
obj = klass.allocate
|
||||
obj.session = double('session', platform: 'linux')
|
||||
allow(obj.session).to receive(:escape_arg) { |arg| "'#{arg}'" }
|
||||
obj
|
||||
end
|
||||
|
||||
context 'on Windows' do
|
||||
before(:each) do
|
||||
allow(subject.session).to receive(:platform).and_return('windows')
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject.find_writable_directories }.to raise_error(RuntimeError, /does not support Windows/)
|
||||
end
|
||||
end
|
||||
|
||||
it 'raises an error for relative paths' do
|
||||
expect { subject.find_writable_directories(path: 'relative/path') }.to raise_error(ArgumentError, /absolute path/)
|
||||
end
|
||||
|
||||
it 'raises an error for negative max_depth' do
|
||||
expect { subject.find_writable_directories(max_depth: -1) }.to raise_error(ArgumentError, /max_depth must not be negative/)
|
||||
end
|
||||
|
||||
context 'on Unix' do
|
||||
|
||||
it 'returns writable directories' do
|
||||
allow(subject).to receive(:cmd_exec).and_return("/tmp\n/var/tmp\n")
|
||||
expect(subject.find_writable_directories).to eq(['/tmp', '/var/tmp'])
|
||||
end
|
||||
|
||||
it 'filters out non-absolute paths and error lines' do
|
||||
allow(subject).to receive(:cmd_exec).and_return("/tmp\nfind: permission denied\n/var/tmp\n")
|
||||
expect(subject.find_writable_directories).to eq(['/tmp', '/var/tmp'])
|
||||
end
|
||||
|
||||
it 'returns an empty array when no directories are found' do
|
||||
allow(subject).to receive(:cmd_exec).and_return('')
|
||||
expect(subject.find_writable_directories).to eq([])
|
||||
end
|
||||
|
||||
it 'passes the timeout to cmd_exec' do
|
||||
expect(subject).to receive(:cmd_exec).with("find '/' -maxdepth 2 -type d -writable 2>/dev/null", nil, 15).and_return("/tmp\n")
|
||||
subject.find_writable_directories
|
||||
end
|
||||
|
||||
it 'passes a custom timeout to cmd_exec' do
|
||||
expect(subject).to receive(:cmd_exec).with("find '/' -maxdepth 2 -type d -writable 2>/dev/null", nil, 60).and_return("/tmp\n")
|
||||
subject.find_writable_directories(timeout: 60)
|
||||
end
|
||||
|
||||
it 'uses default timeout when timeout is 0' do
|
||||
expect(subject).to receive(:cmd_exec).with("find '/' -maxdepth 2 -type d -writable 2>/dev/null", nil, 15).and_return("/tmp\n")
|
||||
subject.find_writable_directories(timeout: 0)
|
||||
end
|
||||
|
||||
it 'warns when max_depth is greater than 2' do
|
||||
expect(subject).to receive(:print_warning).with(/Large max_depth/)
|
||||
allow(subject).to receive(:cmd_exec).and_return("/tmp\n")
|
||||
subject.find_writable_directories(max_depth: 5)
|
||||
end
|
||||
|
||||
it 'does not warn when max_depth is 2 or less' do
|
||||
expect(subject).not_to receive(:print_warning)
|
||||
allow(subject).to receive(:cmd_exec).and_return("/tmp\n")
|
||||
subject.find_writable_directories(max_depth: 2)
|
||||
end
|
||||
|
||||
it 'passes -maxdepth 0 to search only the base directory' do
|
||||
expect(subject).to receive(:cmd_exec).with("find '/' -maxdepth 0 -type d -writable 2>/dev/null", nil, 15).and_return("/\n")
|
||||
expect(subject.find_writable_directories(max_depth: 0)).to eq(['/'])
|
||||
end
|
||||
|
||||
it 'uses custom path and max_depth' do
|
||||
allow(subject).to receive(:print_warning)
|
||||
expect(subject).to receive(:cmd_exec).with("find '/var' -maxdepth 3 -type d -writable 2>/dev/null", nil, 15).and_return("/var/tmp\n")
|
||||
expect(subject.find_writable_directories(path: '/var', max_depth: 3)).to eq(['/var/tmp'])
|
||||
end
|
||||
|
||||
it 'returns nil on failure' do
|
||||
allow(subject).to receive(:cmd_exec).and_raise(RuntimeError, 'connection failed')
|
||||
allow(subject).to receive(:print_error)
|
||||
allow(subject).to receive(:elog)
|
||||
expect(subject.find_writable_directories).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user