@@ -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