Skip to content

Commit 1bfb8ad

Browse files
authored
Merge pull request #21573 from jheysel-r7/feat/lib/post_windows_writable
Add Support for Windows .writable? in Post Mixin
2 parents 394345f + 0a72005 commit 1bfb8ad

3 files changed

Lines changed: 295 additions & 30 deletions

File tree

lib/msf/core/post/file.rb

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -245,13 +245,19 @@ def executable?(path)
245245
# @return [Boolean] true if +path+ exists and is writable
246246
#
247247
def writable?(path)
248-
verification_token = Rex::Text.rand_text_alpha_upper(8)
249-
if session.type == 'powershell' && file?(path)
250-
return cmd_exec("$a=[System.IO.File]::OpenWrite('#{path}');if($?){echo #{verification_token}};$a.Close()").include?(verification_token)
248+
if session.type == 'powershell'
249+
return _writable_powershell?(path)
250+
end
251+
252+
if session.platform == 'windows'
253+
if session.type == 'meterpreter'
254+
return _writable_windows_meterpreter?(path)
255+
end
256+
257+
return _writable_windows_shell?(path)
251258
end
252-
raise "`writable?' method does not support Windows systems" if session.platform == 'windows'
253259

254-
cmd_exec("(test -w '#{path}' || test -O '#{path}') && echo true").to_s.include? 'true'
260+
_writable_unix?(path)
255261
end
256262

257263
#
@@ -709,7 +715,7 @@ def copy_file(src_file, dst_file)
709715
#
710716
# @param path [String] Absolute base path to search from (default: '/')
711717
# @param max_depth [Integer] Maximum directory depth to search (0 = base directory only)
712-
# @param timeout [Integer] Maximum seconds for cmd_exec to wait (default: 15).
718+
# @param timeout [Integer] Maximum seconds for create_process to wait (default: 15).
713719
# Note: if the command times out, the remote find process may continue
714720
# running and tie up the shell channel until it finishes.
715721
# @return [Array<String>, nil] Array of writable directory paths, or nil on failure
@@ -729,18 +735,12 @@ def find_writable_directories(path: '/', max_depth: 2, timeout: 15)
729735
print_warning("Large max_depth (#{max_depth}) may cause the find command to run for a long time and hang the session")
730736
end
731737

732-
escaped_path = session.escape_arg(path)
733-
find_args = ["find #{escaped_path}"]
734-
find_args << "-maxdepth #{max_depth}"
735-
find_args << '-type d'
736-
find_args << '-writable'
737-
738-
find_args << '2>/dev/null'
739-
cmd = find_args.join(' ')
740738
exec_timeout = timeout > 0 ? timeout : 15
739+
find_args = ['-maxdepth', max_depth.to_s, '-type', 'd', '-writable']
741740

742741
begin
743-
cmd_exec(cmd, nil, exec_timeout).to_s.lines.map(&:strip).select { |p| p.start_with?('/') }
742+
output = create_process('find', args: [path] + find_args, time_out: exec_timeout)
743+
output.to_s.lines.map(&:strip).select { |p| p.start_with?('/') }
744744
rescue ::StandardError => e
745745
elog("Failed to find writable directories in #{path}", error: e)
746746
print_error("Failed to find writable directories in #{path}")
@@ -750,6 +750,73 @@ def find_writable_directories(path: '/', max_depth: 2, timeout: 15)
750750

751751
protected
752752

753+
# Check writability via PowerShell session by attempting to create/open a file.
754+
#
755+
# @param path [String] Remote path to check
756+
# @return [Boolean] true if +path+ is writable
757+
def _writable_powershell?(path)
758+
verification_token = Rex::Text.rand_text_alpha_upper(8)
759+
if directory?(path)
760+
tmp_file = "#{path}\\#{Rex::Text.rand_text_alpha(8)}.tmp"
761+
script = "$f=[System.IO.File]::Create('#{tmp_file}');if($?){$f.Close();[System.IO.File]::Delete('#{tmp_file}');echo #{verification_token}}"
762+
return create_process('powershell.exe', args: ['-NoProfile', '-Command', script]).to_s.include?(verification_token)
763+
end
764+
return false unless file?(path)
765+
766+
script = "$a=[System.IO.File]::OpenWrite('#{path}');if($?){echo #{verification_token}};$a.Close()"
767+
create_process('powershell.exe', args: ['-NoProfile', '-Command', script]).to_s.include?(verification_token)
768+
end
769+
770+
# Check writability on Windows via Meterpreter by attempting to open a file handle.
771+
#
772+
# @param path [String] Remote path to check
773+
# @return [Boolean] true if +path+ is writable
774+
def _writable_windows_meterpreter?(path)
775+
if directory?(path)
776+
tmp_file = "#{path}\\#{Rex::Text.rand_text_alpha(8)}.tmp"
777+
begin
778+
fd = session.fs.file.new(tmp_file, 'wb')
779+
fd.close
780+
session.fs.file.rm(tmp_file)
781+
return true
782+
rescue ::Rex::Post::Meterpreter::RequestError
783+
return false
784+
end
785+
end
786+
return false unless file?(path)
787+
788+
begin
789+
fd = session.fs.file.new(path, 'wb')
790+
fd.close
791+
true
792+
rescue ::Rex::Post::Meterpreter::RequestError
793+
false
794+
end
795+
end
796+
797+
# Check writability on Windows via a shell session using cmd.exe redirects.
798+
#
799+
# @param path [String] Remote path to check
800+
# @return [Boolean] true if +path+ is writable
801+
def _writable_windows_shell?(path)
802+
verification_token = Rex::Text.rand_text_alpha_upper(8)
803+
if directory?(path)
804+
tmp_file = "#{path}\\#{Rex::Text.rand_text_alpha(8)}.tmp"
805+
return create_process('cmd.exe', args: ['/C', "type nul >> \"#{tmp_file}\" 2>nul && del \"#{tmp_file}\" && echo #{verification_token}"]).to_s.include?(verification_token)
806+
end
807+
return false unless file?(path)
808+
809+
create_process('cmd.exe', args: ['/C', "type nul >> \"#{path}\" 2>nul && echo #{verification_token}"]).to_s.include?(verification_token)
810+
end
811+
812+
# Check writability on Unix using test(1) builtins.
813+
#
814+
# @param path [String] Remote path to check
815+
# @return [Boolean] true if +path+ is writable
816+
def _writable_unix?(path)
817+
create_process('sh', args: ['-c', "(test -w '#{path}' || test -O '#{path}') && echo true"]).to_s.include?('true')
818+
end
819+
753820
def _append_file_powershell(file_name, data)
754821
_write_file_powershell(file_name, data, true)
755822
end

0 commit comments

Comments
 (0)