CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/rex/post/meterpreter/extensions/stdapi/fs/file.rb
Views: 11795
1
# -*- coding: binary -*-
2
3
require 'rex/post/file'
4
require 'rex/post/meterpreter/channel'
5
require 'rex/post/meterpreter/channels/pools/file'
6
require 'rex/post/meterpreter/extensions/stdapi/stdapi'
7
require 'rex/post/meterpreter/extensions/stdapi/fs/io'
8
require 'rex/post/meterpreter/extensions/stdapi/fs/file_stat'
9
require 'fileutils'
10
require 'filesize'
11
12
module Rex
13
module Post
14
module Meterpreter
15
module Extensions
16
module Stdapi
17
module Fs
18
19
###
20
#
21
# This class implements the Rex::Post::File interface and wraps interaction
22
# with files on the remote machine.
23
#
24
###
25
class File < Rex::Post::Meterpreter::Extensions::Stdapi::Fs::IO
26
27
include Rex::Post::File
28
29
MIN_BLOCK_SIZE = 1024
30
31
class << self
32
attr_accessor :client
33
end
34
35
#
36
# Return the directory separator, i.e.: "/" on unix, "\\" on windows
37
#
38
def File.separator()
39
# The separator won't change, so cache it to prevent sending
40
# unnecessary requests.
41
return @separator if @separator
42
43
request = Packet.create_request(COMMAND_ID_STDAPI_FS_SEPARATOR)
44
45
# Fall back to the old behavior of always assuming windows. This
46
# allows meterpreter executables built before the addition of this
47
# command to continue functioning.
48
begin
49
response = client.send_request(request)
50
@separator = response.get_tlv_value(TLV_TYPE_STRING)
51
rescue RequestError
52
@separator = "\\"
53
end
54
55
return @separator
56
end
57
58
class << self
59
alias :Separator :separator
60
alias :SEPARATOR :separator
61
end
62
63
#
64
# Search for files matching +glob+ starting in directory +root+.
65
#
66
# Returns an Array (possibly empty) of Hashes. Each element has the following
67
# keys:
68
# 'path':: The directory in which the file was found
69
# 'name':: File name
70
# 'size':: Size of the file, in bytes
71
#
72
# Example:
73
# client.fs.file.search(client.fs.dir.pwd, "*.txt")
74
# # => [{"path"=>"C:\\Documents and Settings\\user\\Desktop", "name"=>"foo.txt", "size"=>0}]
75
#
76
# Raises a RequestError if +root+ is not a directory.
77
#
78
def File.search( root=nil, glob="*.*", recurse=true, timeout=-1, modified_start_date=nil, modified_end_date=nil)
79
80
files = ::Array.new
81
82
request = Packet.create_request( COMMAND_ID_STDAPI_FS_SEARCH )
83
84
root = client.unicode_filter_decode(root) if root
85
root = root.chomp( self.separator ) if root && !root.eql?('/')
86
87
request.add_tlv( TLV_TYPE_SEARCH_ROOT, root )
88
request.add_tlv( TLV_TYPE_SEARCH_GLOB, glob )
89
request.add_tlv( TLV_TYPE_SEARCH_RECURSE, recurse )
90
request.add_tlv( TLV_TYPE_SEARCH_M_START_DATE, modified_start_date) if modified_start_date
91
request.add_tlv( TLV_TYPE_SEARCH_M_END_DATE, modified_end_date) if modified_end_date
92
93
# we set the response timeout to -1 to wait indefinitely as a
94
# search could take an indeterminate amount of time to complete.
95
response = client.send_request( request, timeout )
96
if( response.result == 0 )
97
response.each( TLV_TYPE_SEARCH_RESULTS ) do | results |
98
files << {
99
'path' => client.unicode_filter_encode(results.get_tlv_value(TLV_TYPE_FILE_PATH).chomp( self.separator )),
100
'name' => client.unicode_filter_encode(results.get_tlv_value(TLV_TYPE_FILE_NAME)),
101
'size' => results.get_tlv_value(TLV_TYPE_FILE_SIZE),
102
'mtime'=> results.get_tlv_value(TLV_TYPE_SEARCH_MTIME)
103
}
104
end
105
end
106
107
return files
108
end
109
110
#
111
# Returns the base name of the supplied file path to the caller.
112
#
113
def File.basename(*a)
114
path = a[0]
115
116
# Allow both kinds of dir serparators since lots and lots of code
117
# assumes one or the other so this ends up getting called with strings
118
# like: "C:\\foo/bar"
119
path =~ %r#.*[/\\](.*)$#
120
121
Rex::FileUtils.clean_path($1 || path)
122
end
123
124
#
125
# Expands a file path, substituting all environment variables, such as
126
# %TEMP% on Windows or $HOME on Unix
127
#
128
# Examples:
129
# client.fs.file.expand_path("%appdata%")
130
# # => "C:\\Documents and Settings\\user\\Application Data"
131
# client.fs.file.expand_path("~")
132
# # => "/home/user"
133
# client.fs.file.expand_path("$HOME/dir")
134
# # => "/home/user/dir"
135
# client.fs.file.expand_path("asdf")
136
# # => "asdf"
137
#
138
def File.expand_path(path)
139
case client.platform
140
when 'osx', 'freebsd', 'bsd', 'linux', 'android', 'apple_ios'
141
# For unix-based systems, do some of the work here
142
# First check for ~
143
path_components = path.split(separator)
144
if path_components.length > 0 && path_components[0] == '~'
145
path = "$HOME#{path[1..-1]}"
146
end
147
148
# Now find the environment variables we'll need from the client
149
env_regex = /\$(?:([A-Za-z0-9_]+)|\{([A-Za-z0-9_]+)\})/
150
matches = path.to_enum(:scan, env_regex).map { Regexp.last_match }
151
env_vars = matches.map { |match| (match[1] || match[2]).to_s }.uniq
152
153
# Retrieve them
154
env_vals = client.sys.config.getenvs(*env_vars)
155
156
# Now fill them in
157
path.gsub(env_regex) { |_z| envvar = $1; envvar = $2 if envvar == nil; env_vals[envvar] }
158
else
159
request = Packet.create_request(COMMAND_ID_STDAPI_FS_FILE_EXPAND_PATH)
160
161
request.add_tlv(TLV_TYPE_FILE_PATH, client.unicode_filter_decode( path ))
162
163
response = client.send_request(request)
164
165
return client.unicode_filter_encode(response.get_tlv_value(TLV_TYPE_FILE_PATH))
166
end
167
end
168
169
170
#
171
# Calculates the MD5 (16-bytes raw) of a remote file
172
#
173
def File.md5(path)
174
request = Packet.create_request(COMMAND_ID_STDAPI_FS_MD5)
175
176
request.add_tlv(TLV_TYPE_FILE_PATH, client.unicode_filter_decode( path ))
177
178
response = client.send_request(request)
179
180
# older meterpreter binaries will send FILE_NAME containing the hash
181
hash = response.get_tlv_value(TLV_TYPE_FILE_HASH) ||
182
response.get_tlv_value(TLV_TYPE_FILE_NAME)
183
return hash
184
end
185
186
#
187
# Calculates the SHA1 (20-bytes raw) of a remote file
188
#
189
def File.sha1(path)
190
request = Packet.create_request(COMMAND_ID_STDAPI_FS_SHA1)
191
192
request.add_tlv(TLV_TYPE_FILE_PATH, client.unicode_filter_decode( path ))
193
194
response = client.send_request(request)
195
196
# older meterpreter binaries will send FILE_NAME containing the hash
197
hash = response.get_tlv_value(TLV_TYPE_FILE_HASH) ||
198
response.get_tlv_value(TLV_TYPE_FILE_NAME)
199
return hash
200
end
201
202
#
203
# Performs a stat on a file and returns a FileStat instance.
204
#
205
def File.stat(name)
206
return client.fs.filestat.new( name )
207
end
208
209
#
210
# Returns true if the remote file +name+ exists, false otherwise
211
#
212
def File.exist?(name)
213
r = client.fs.filestat.new(name) rescue nil
214
r ? true : false
215
end
216
217
#
218
# Performs a delete on the remote file +name+
219
#
220
def File.rm(name)
221
request = Packet.create_request(COMMAND_ID_STDAPI_FS_DELETE_FILE)
222
223
request.add_tlv(TLV_TYPE_FILE_PATH, client.unicode_filter_decode( name ))
224
225
response = client.send_request(request)
226
227
return response
228
end
229
230
class << self
231
alias unlink rm
232
alias delete rm
233
end
234
235
#
236
# Performs a rename from oldname to newname
237
#
238
def File.mv(oldname, newname)
239
request = Packet.create_request(COMMAND_ID_STDAPI_FS_FILE_MOVE)
240
241
request.add_tlv(TLV_TYPE_FILE_NAME, client.unicode_filter_decode( oldname ))
242
request.add_tlv(TLV_TYPE_FILE_PATH, client.unicode_filter_decode( newname ))
243
244
response = client.send_request(request)
245
246
return response
247
end
248
249
class << self
250
alias move mv
251
alias rename mv
252
end
253
254
#
255
# Performs a copy from oldname to newname
256
#
257
def File.cp(oldname, newname)
258
request = Packet.create_request(COMMAND_ID_STDAPI_FS_FILE_COPY)
259
260
request.add_tlv(TLV_TYPE_FILE_NAME, client.unicode_filter_decode( oldname ))
261
request.add_tlv(TLV_TYPE_FILE_PATH, client.unicode_filter_decode( newname ))
262
263
response = client.send_request(request)
264
265
return response
266
end
267
268
class << self
269
alias copy cp
270
end
271
272
#
273
# Performs a chmod on the remote file
274
#
275
def File.chmod(name, mode)
276
request = Packet.create_request(COMMAND_ID_STDAPI_FS_CHMOD)
277
278
request.add_tlv(TLV_TYPE_FILE_PATH, client.unicode_filter_decode( name ))
279
request.add_tlv(TLV_TYPE_FILE_MODE_T, mode)
280
281
response = client.send_request(request)
282
283
return response
284
end
285
286
#
287
# Upload one or more files to the remote remote directory supplied in
288
# +destination+.
289
#
290
# If a block is given, it will be called before each file is uploaded and
291
# again when each upload is complete.
292
#
293
def File.upload(dest, *src_files, &stat)
294
src_files.each { |src|
295
if (self.basename(dest) != ::File.basename(src))
296
dest += self.separator unless dest.end_with?(self.separator)
297
dest += ::File.basename(src)
298
end
299
stat.call('Uploading', src, dest) if (stat)
300
301
upload_file(dest, src)
302
stat.call('Completed', src, dest) if (stat)
303
}
304
end
305
306
#
307
# Upload a single file.
308
#
309
def File.upload_file(dest_file, src_file, &stat)
310
# Open the file on the remote side for writing and read
311
# all of the contents of the local file
312
stat.call('Uploading', src_file, dest_file) if stat
313
dest_fd = nil
314
src_fd = nil
315
buf_size = 8 * 1024 * 1024
316
317
begin
318
dest_fd = client.fs.file.new(dest_file, "wb")
319
src_fd = ::File.open(src_file, "rb")
320
src_size = src_fd.stat.size
321
while (buf = src_fd.read(buf_size))
322
dest_fd.write(buf)
323
percent = dest_fd.pos.to_f / src_size.to_f * 100.0
324
msg = "Uploaded #{Filesize.new(dest_fd.pos).pretty} of " \
325
"#{Filesize.new(src_size).pretty} (#{percent.round(2)}%)"
326
stat.call(msg, src_file, dest_file) if stat
327
end
328
ensure
329
src_fd.close unless src_fd.nil?
330
dest_fd.close unless dest_fd.nil?
331
end
332
stat.call('Completed', src_file, dest_file) if stat
333
end
334
335
def File.is_glob?(name)
336
/\*|\[|\?/ === name
337
end
338
339
#
340
# Download one or more files from the remote computer to the local
341
# directory supplied in destination.
342
#
343
# If a block is given, it will be called before each file is downloaded and
344
# again when each download is complete.
345
#
346
def File.download(dest, src_files, opts = {}, &stat)
347
dest.force_encoding('UTF-8')
348
timestamp = opts["timestamp"]
349
[*src_files].each { |src|
350
src.force_encoding('UTF-8')
351
if (::File.basename(dest) != File.basename(src))
352
# The destination when downloading is a local file so use this
353
# system's separator
354
dest += ::File::SEPARATOR unless dest.end_with?(::File::SEPARATOR)
355
dest += File.basename(src)
356
end
357
358
# XXX: dest can be the same object as src, so we use += instead of <<
359
if timestamp
360
dest += timestamp
361
end
362
363
stat.call('Downloading', src, dest) if (stat)
364
result = download_file(dest, src, opts, &stat)
365
stat.call(result, src, dest) if (stat)
366
}
367
end
368
369
#
370
# Download a single file.
371
#
372
def File.download_file(dest_file, src_file, opts = {}, &stat)
373
stat ||= lambda { |a,b,c| }
374
375
adaptive = opts["adaptive"]
376
block_size = opts["block_size"] || 1024 * 1024
377
continue = opts["continue"]
378
tries_no = opts["tries_no"]
379
tries = opts["tries"]
380
381
src_fd = client.fs.file.new(src_file, "rb")
382
383
# Check for changes
384
src_stat = client.fs.filestat.new(src_file)
385
if ::File.exist?(dest_file)
386
dst_stat = ::File.stat(dest_file)
387
if src_stat.size == dst_stat.size && src_stat.mtime == dst_stat.mtime
388
src_fd.close
389
return 'Skipped'
390
end
391
end
392
393
# Make the destination path if necessary
394
dir = ::File.dirname(dest_file)
395
::FileUtils.mkdir_p(dir) if dir and not ::File.directory?(dir)
396
397
src_size = Filesize.new(src_stat.size).pretty
398
399
if continue
400
# continue downloading the file - skip downloaded part in the source
401
dst_fd = ::File.new(dest_file, "ab")
402
begin
403
dst_fd.seek(0, ::IO::SEEK_END)
404
in_pos = dst_fd.pos
405
src_fd.seek(in_pos)
406
stat.call("Continuing from #{Filesize.new(in_pos).pretty} of #{src_size}", src_file, dest_file)
407
rescue
408
# if we can't seek, download again
409
stat.call('Error continuing - downloading from scratch', src_file, dest_file)
410
dst_fd.close
411
dst_fd = ::File.new(dest_file, "wb")
412
end
413
else
414
dst_fd = ::File.new(dest_file, "wb")
415
end
416
417
# Keep transferring until EOF is reached...
418
begin
419
if tries
420
# resume when timeouts encountered
421
seek_back = false
422
adjust_block = false
423
tries_cnt = 0
424
begin # while
425
begin # exception
426
if seek_back
427
in_pos = dst_fd.pos
428
src_fd.seek(in_pos)
429
seek_back = false
430
stat.call("Resuming at #{Filesize.new(in_pos).pretty} of #{src_size}", src_file, dest_file)
431
else
432
# successfully read and wrote - reset the counter
433
tries_cnt = 0
434
end
435
adjust_block = true
436
data = src_fd.read(block_size)
437
adjust_block = false
438
rescue Rex::TimeoutError
439
# timeout encountered - either seek back and retry or quit
440
if (tries && (tries_no == 0 || tries_cnt < tries_no))
441
tries_cnt += 1
442
seek_back = true
443
# try a smaller block size for the next round
444
if adaptive && adjust_block
445
block_size = [block_size >> 1, MIN_BLOCK_SIZE].max
446
adjust_block = false
447
msg = "Error downloading, block size set to #{block_size} - retry # #{tries_cnt}"
448
stat.call(msg, src_file, dest_file)
449
else
450
stat.call("Error downloading - retry # #{tries_cnt}", src_file, dest_file)
451
end
452
retry
453
else
454
stat.call('Error downloading - giving up', src_file, dest_file)
455
raise
456
end
457
end
458
dst_fd.write(data) if (data != nil)
459
percent = dst_fd.pos.to_f / src_stat.size.to_f * 100.0
460
msg = "Downloaded #{Filesize.new(dst_fd.pos).pretty} of #{src_size} (#{percent.round(2)}%)"
461
stat.call(msg, src_file, dest_file)
462
end while (data != nil)
463
else
464
# do the simple copying quitting on the first error
465
while ((data = src_fd.read(block_size)) != nil)
466
dst_fd.write(data)
467
percent = dst_fd.pos.to_f / src_stat.size.to_f * 100.0
468
msg = "Downloaded #{Filesize.new(dst_fd.pos).pretty} of #{src_size} (#{percent.round(2)}%)"
469
stat.call(msg, src_file, dest_file)
470
end
471
end
472
rescue EOFError
473
ensure
474
src_fd.close
475
dst_fd.close
476
end
477
478
# Clone the times from the remote file
479
::File.utime(src_stat.atime, src_stat.mtime, dest_file)
480
return 'Completed'
481
end
482
483
#
484
# With no associated block, File.open is a synonym for ::new. If the optional
485
# code block is given, it will be passed the opened file as an argument, and
486
# the File object will automatically be closed when the block terminates. In
487
# this instance, File.open returns the value of the block.
488
#
489
# (doc stolen from http://www.ruby-doc.org/core-1.9.3/File.html#method-c-open)
490
#
491
def File.open(name, mode="r", perms=0)
492
f = new(name, mode, perms)
493
if block_given?
494
ret = yield f
495
f.close
496
return ret
497
else
498
return f
499
end
500
end
501
502
##
503
#
504
# Constructor
505
#
506
##
507
508
#
509
# Initializes and opens the specified file with the specified permissions.
510
#
511
def initialize(name, mode = "r", perms = 0)
512
self.client = self.class.client
513
self.filed = _open(name, mode, perms)
514
end
515
516
##
517
#
518
# IO implementers
519
#
520
##
521
522
#
523
# Returns whether or not the file has reach EOF.
524
#
525
def eof
526
return self.filed.eof
527
end
528
529
#
530
# Returns the current position of the file pointer.
531
#
532
def pos
533
return self.filed.tell
534
end
535
536
#
537
# Synonym for sysseek.
538
#
539
def seek(offset, whence = ::IO::SEEK_SET)
540
return self.sysseek(offset, whence)
541
end
542
543
#
544
# Seeks to the supplied offset based on the supplied relativity.
545
#
546
def sysseek(offset, whence = ::IO::SEEK_SET)
547
return self.filed.seek(offset, whence)
548
end
549
550
protected
551
552
##
553
#
554
# Internal methods
555
#
556
##
557
558
#
559
# Creates a File channel using the supplied information.
560
#
561
def _open(name, mode = "r", perms = 0)
562
return Rex::Post::Meterpreter::Channels::Pools::File.open(
563
self.client, name, mode, perms)
564
end
565
566
attr_accessor :client # :nodoc:
567
568
end
569
570
end; end; end; end; end; end
571
572
573