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