Merge pull request #21232 from bcoles/file-find_writable_directories

Add find_writable_directories to Msf::Post::File
This commit is contained in:
Brendan
2026-04-20 16:33:53 -05:00
committed by GitHub
2 changed files with 146 additions and 1 deletions
+46
View File
@@ -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)
+100 -1
View File
@@ -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