Module: Msf::Exploit::Remote::Ftp

Includes:
Auxiliary::Report, Tcp
Defined in:
lib/msf/core/exploit/remote/ftp.rb

Overview

This module exposes methods that may be useful to exploits that deal with servers that speak the File Transfer Protocol (FTP).

Instance Attribute Summary collapse

Attributes included from Tcp

#sock

Instance Method Summary collapse

Methods included from Auxiliary::Report

#active_db?, #create_cracked_credential, #create_credential, #create_credential_and_login, #create_credential_login, #db, #db_warning_given?, #get_client, #get_host, #inside_workspace_boundary?, #invalidate_login, #mytask, #myworkspace, #myworkspace_id, #report_auth_info, #report_client, #report_exploit, #report_host, #report_loot, #report_note, #report_service, #report_vuln, #report_web_form, #report_web_page, #report_web_site, #report_web_vuln, #store_cred, #store_local, #store_loot

Methods included from Metasploit::Framework::Require

optionally, optionally_active_record_railtie, optionally_include_metasploit_credential_creation, #optionally_include_metasploit_credential_creation, optionally_require_metasploit_db_gem_engines

Methods included from Tcp

#chost, #cleanup, #connect_timeout, #cport, #disconnect, #handler, #lhost, #lport, #peer, #print_prefix, #proxies, #replicant, #rhost, #rport, #set_tcp_evasions, #shutdown, #ssl, #ssl_cipher, #ssl_verify_mode, #ssl_version, #sslkeylogfile

Instance Attribute Details

This attribute holds the banner that was read in after a successful call to connect or connect_login.



649
650
651
# File 'lib/msf/core/exploit/remote/ftp.rb', line 649

def banner
  @banner
end

#datasocket ⇒ Object (protected)

This attribute holds the banner that was read in after a successful call to connect or connect_login.



649
650
651
# File 'lib/msf/core/exploit/remote/ftp.rb', line 649

def datasocket
  @datasocket
end

Instance Method Details

Returns a normalised version string from the FTP banner Uses Recog - falls back to regex if no match

Returns:

  • (String, nil) —

    version string, or nil if no banner



164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/msf/core/exploit/remote/ftp.rb', line 164

def banner_version
  info = recog_banner
  return [info['service.product'], info['service.version']].compact.join(' ') if info

  # 220 (vsFTPd 2.3.4)\x0d\x0a                                    -> vsFTPd 2.3.4
  # 220 ProFTPD 1.3.1 Server (Debian) [::ffff:192.0.2.10]\x0d\x0a -> ProFTPD 1.3.1 Server (Debian)
  banner.to_s
        .sub(/^\d{3}[\s-]/, '')
        .strip
        .gsub(/\A\(|\)\z/, '')
        .gsub(/\s*\[(?:(?:\d{1,3}\.){3}\d{1,3}|[0-9A-Fa-f:]*:[0-9A-Fa-f:.]+)\]/, '')
end

#connect(global = true, verbose = nil) ⇒ Object Also known as: ftp_connect

This method establishes an FTP connection to host and port specified by the 'rhost' and 'rport' methods. After connecting, the banner message is read in and stored in the 'banner' attribute.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/msf/core/exploit/remote/ftp.rb', line 50

def connect(global = true, verbose = nil)
  verbose = datastore['FTPDEBUG'] || datastore['VERBOSE'] if verbose.nil?

  print_status("Connecting to FTP server...") if verbose

  begin
    fd = super(global)
  rescue ::Rex::ConnectionRefused
    report_host(host: rhost)
    raise
  end

  # Wait for a banner to arrive...
  self.banner = recv_ftp_resp(fd)
  remove_instance_variable(:@recog_banner) if instance_variable_defined?(:@recog_banner)

  print_status('Connected to target FTP server') if verbose

  # Only record the service and banner when the greeting looks like FTP (RFC 959)
  if self.banner&.match?(/^(120|220)[\s-]/)
    # Cleaned up FTP banner
    report_service(
      host: rhost,
      port: rport,
      proto: 'tcp',
      name: 'ftp',
      info: Rex::Text.to_hex_ascii(banner_version),
      parents: {
        host: rhost,
        port: rport,
        proto: 'tcp',
        name: 'tcp'
      }
    )

    # Raw FTP banner
    report_note(
      host: rhost,
      port: rport,
      proto: 'tcp',
      sname: 'ftp',
      type: 'ftp.banner',
      data: { banner: Rex::Text.to_hex_ascii(self.banner.strip) }
    )

    # Lookup FTP banner
    info = recog_banner
    if info
      os_info = {}
      info.each_pair do |k, v|
        case k
        when 'os.product' then os_info[:os_name] = v
        when 'os.vendor' then os_info[:os_flavor] = v
        when 'os.version' then os_info[:os_sp] = v
        when 'os.cpe23', 'service.cpe23'
          report_note(
            host: rhost,
            port: rport,
            proto: 'tcp',
            sname: 'ftp',
            type: 'ftp.cpe',
            data: { cpe: v },
            update: :unique_data
          )
        end
      end

      report_host({ host: rhost }.merge(os_info)) unless os_info.empty?
    end
  else
    report_service(
      host: rhost,
      port: rport,
      proto: 'tcp',
      name: 'unknown',
      info: Rex::Text.to_hex_ascii((self.banner || @ftpbuff).to_s.strip).presence || 'Non-FTP service',
      parents: {
        host: rhost,
        port: rport,
        proto: 'tcp',
        name: 'tcp'
      }
    )
  end

  # Return the file descriptor to the caller
  fd
end

#connect_login(global = true, verbose = nil) ⇒ Object

Connect and login to the remote FTP server using the credentials that have been supplied in the exploit options.



373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/msf/core/exploit/remote/ftp.rb', line 373

def (global = true, verbose = nil)
  verbose = datastore['FTPDEBUG'] || datastore['VERBOSE'] if verbose.nil?

  ftpsock = ftp_connect(global, verbose)

  if !(user and pass)
    print_error("No username and password were supplied, unable to login")
    return false
  end

  print_status("Authenticating as #{user} with password #{pass}...") if verbose
  res = send_user(user, ftpsock)

  if (res !~ /^(331|2)/)
    print_error("The server rejected our username") if verbose
    return false
  end

  if (pass)
    print_status("Sending password...") if verbose
    res = send_pass(pass, ftpsock)
    if (res !~ /^2/)
      print_error("The server rejected our password") if verbose
      return false
    end
  end

  return true
end

#data_connect(mode = nil, nsock = self.sock) ⇒ Object

This method handles establishing datasocket for data channel



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/msf/core/exploit/remote/ftp.rb', line 270

def data_connect(mode = nil, nsock = self.sock)
  pass_mode = datastore['PassiveMode']

  if mode
    res = send_cmd([ 'TYPE' , mode ], true, nsock)
    return nil if not res =~ /^200/
  end

  # force datasocket to renegotiate
  if datasocket
    old_datasocket = datasocket
    self.datasocket = nil
    old_datasocket.shutdown
    old_datasocket.close
  end

  # Need to be able to do both extended and normal
  # passive modes.  normal passive mode is default
  # details of EPSV are in RFC2428
  # pass_mode = true is EPSV; false is PASV
  if pass_mode
    res = send_cmd(['EPSV'], true, nsock)
    unless res&.match?(/^229/)
      vprint_warning('FTP server returned an unexpected response to EPSV')
      return nil
    end
    # 229 Entering Passive Mode (|||port|)
    match = res.match(/\(\|\|\|(\d+)\|\)/)
    unless match
      vprint_warning('FTP server returned a malformed EPSV response')
      return nil
    end

    dataport = match[1].to_i
    unless dataport.between?(1, 65_535)
      vprint_warning("FTP server returned an invalid EPSV port: #{dataport}")
      return nil
    end

    self.datasocket = Rex::Socket::Tcp.create(
      'PeerHost' => nsock.peerhost,
      'PeerPort' => dataport,
      'Context'  => { 'Msf' => framework, 'MsfExploit' => self }
    )
  else
    res = send_cmd(['PASV'], true, nsock)
    unless res&.match?(/^227/)
      vprint_warning('FTP server returned an unexpected response to PASV')
      return nil
    end
    # 227 Entering Passive Mode (127,0,0,1,196,5)
    match = res.match(/\((\d{1,3}),(\d{1,3}),(\d{1,3}),(\d{1,3}),(\d{1,3}),(\d{1,3})\)/)
    unless match
      vprint_warning('FTP server returned a malformed PASV response')
      return nil
    end

    values = match.captures.map(&:to_i)
    unless values.all? { |value| value.between?(0, 255) }
      vprint_warning('FTP server returned a PASV value outside the byte range')
      return nil
    end

    dataport = (values[4] * 256) + values[5]
    if dataport.zero?
      vprint_warning('FTP server returned an invalid PASV port: 0')
      return nil
    end

    advertised_host = values.first(4).join('.')
    if advertised_host != nsock.peerhost
      vprint_status("Ignoring PASV advertised host #{advertised_host}; using control connection host #{nsock.peerhost}")
    end

    # Prefer the original control connection host over the host advertised in the PASV response.
    self.datasocket = Rex::Socket::Tcp.create(
      'PeerHost' => nsock.peerhost,
      'PeerPort' => dataport,
      'Context'  => { 'Msf' => framework, 'MsfExploit' => self }
    )
  end

  self.datasocket
end

#data_disconnect ⇒ Object

This method handles disconnecting our data channel



358
359
360
361
362
363
364
365
366
367
# File 'lib/msf/core/exploit/remote/ftp.rb', line 358

def data_disconnect
  begin
    if datasocket
      datasocket.shutdown
      datasocket.close
    end
  rescue IOError
  end
  datasocket = nil if datasocket
end

#ftp_data_timeout ⇒ Object

Returns the number of seconds to wait to get more FTP data



637
638
639
# File 'lib/msf/core/exploit/remote/ftp.rb', line 637

def ftp_data_timeout
  (datastore['FTPDataTimeout'] || 1).to_i
end

#ftp_fingerprint(logged_in_as: 'anonymous') ⇒ void

This method returns an undefined value.

Sends FEAT, STAT, and SYST and records the output as workspace notes Skips SYST-based OS detection if Recog already identified os.product

Parameters:

  • logged_in_as (String) (defaults to: 'anonymous') —

    current FTP session user, recorded in notes

Raises:

  • (Rex::ConnectionError) —

    if the server closes the connection mid-fingerprint



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/msf/core/exploit/remote/ftp.rb', line 184

def ftp_fingerprint(logged_in_as: 'anonymous')
  print_status("Fingerprinting FTP service (as #{logged_in_as})")

  [
    ['FEAT', 'ftp.cmd.feat'], # server-level
    ['STAT', 'ftp.cmd.stat'], # user-level
    ['SYST', 'ftp.cmd.syst'] # server-level
  ].each do |cmd, note_type|
    vprint_status("Sending FTP command: #{cmd}")
    response = send_cmd([cmd], true)
    raise Rex::ConnectionError.new(rhost, rport) unless response
    next if response.empty?

    response.strip.each_line.with_index do |line, i|
      prefix = i == 0 ? "FTP #{cmd}: " : '  '
      vprint_status("#{prefix}#{line.strip}")
    end

    # Examples:
    #   215 UNIX Type: L8
    #   215 Windows_NT
    #   215 UNIX emulated by FileZilla     (Windows host but looks like *nix service)
    if cmd == 'SYST'
      os_name = if response.match?(/emulated/i) then nil
                elsif response.match?(/Windows_NT/i) then 'Windows'
                elsif response.match?(/UNIX/i) then '*nix'
                else nil
                end
      report_host(host: rhost, os_name: os_name) if os_name && !recog_banner&.key?('os.product')
    end

    report_note(
      host: rhost,
      port: rport,
      proto: 'tcp',
      sname: 'ftp',
      type: note_type,
      data: {
        username: logged_in_as,
        output: response.strip
      }
    )
  end
end

#ftp_list_directory(logged_in_as: 'anonymous', save_loot: false) ⇒ String?

Lists the current remote directory and optionally stores the result as loot

Parameters:

  • logged_in_as (String) (defaults to: 'anonymous') —

    current FTP session user, used to label the loot file

  • save_loot (Boolean) (defaults to: false) —

    store the directory listing as loot

Returns:

  • (String, nil) —

    directory listing content, or nil on failure



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/msf/core/exploit/remote/ftp.rb', line 235

def ftp_list_directory(logged_in_as: 'anonymous', save_loot: false)
  print_status('Getting FTP root directory contents')

  listing = send_cmd_data(['LS'], nil)
  if listing.nil?
    print_warning('Could not retrieve directory listing (data connection failed)')
    return nil
  elsif listing[1].blank?
    vprint_status('Directory listing: (empty)')
    return nil
  end

  vprint_status('Directory listing:')
  listing[1].strip.each_line do |line|
    vprint_status("  #{line.strip}")
  end

  return listing[1] unless save_loot

  path = store_loot(
    'ftp.dir_listing',
    'text/plain',
    rhost,
    listing[1],
    "ftp_#{logged_in_as.to_s.downcase}.txt",
    "FTP directory listing for #{logged_in_as}"
  )
  print_good("Directory listing stored to: #{path}")

  listing[1]
end

#ftp_timeout ⇒ Object

Returns the number of seconds to wait for a FTP reply



630
631
632
# File 'lib/msf/core/exploit/remote/ftp.rb', line 630

def ftp_timeout
  (datastore['FTPTimeout'] || 10).to_i
end

#initialize(info = {}) ⇒ Object

Creates an instance of an FTP exploit module.



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/msf/core/exploit/remote/ftp.rb', line 19

def initialize(info = {})
  super

  # Register the options that all FTP exploits may make use of.
  register_options(
    [
      Opt::RHOST,
      Opt::RPORT(21),
      OptString.new('FTPUSER', [ false, 'The username to authenticate as', 'anonymous'], fallbacks: ['USERNAME']),
      OptString.new('FTPPASS', [ false, 'The password for the specified username', 'mozilla@example.com'], fallbacks: ['PASSWORD']),
    ], Msf::Exploit::Remote::Ftp)

  register_advanced_options(
    [
      OptInt.new('FTPTimeout', [ true, 'The number of seconds to wait for a reply from an FTP command', 16]),
      OptBool.new('FTPDEBUG', [ false, 'Whether or not to print verbose debug statements', false ]),
      OptBool.new('PassiveMode', [ false, 'Set true for extended passive (EPSV) ftp mode.', false])
    ], Msf::Exploit::Remote::Ftp)

  register_autofilter_ports([ 21, 2121])
  register_autofilter_services(%W{ ftp })

  @ftpbuff = ""

end

#pass ⇒ Object

Returns the user string from the 'FTPPASS' option.



623
624
625
# File 'lib/msf/core/exploit/remote/ftp.rb', line 623

def pass
  datastore['FTPPASS']
end

#raw_send(cmd, nsock = self.sock) ⇒ Object

This method transmits a FTP command and does not wait for a response



602
603
604
605
# File 'lib/msf/core/exploit/remote/ftp.rb', line 602

def raw_send(cmd, nsock = self.sock)
  print_status("FTP send: #{cmd.inspect}") if datastore['FTPDEBUG']
  nsock.put(cmd)
end

#raw_send_recv(cmd, nsock = self.sock) ⇒ Object

This method transmits a FTP command and waits for a response. If one is received, it is returned to the caller.



509
510
511
512
# File 'lib/msf/core/exploit/remote/ftp.rb', line 509

def raw_send_recv(cmd, nsock = self.sock)
  nsock.put(cmd)
  nsock.get_once(-1, ftp_timeout)
end

#recog_banner ⇒ Hash?

Matches the FTP banner against the Recog ftp.banner fingerprint set Tests each line of a multi-line banner after stripping the reply code prefix Result is memoised

Returns:

  • (Hash, nil) —

    Recog match, or nil if no fingerprint matched



145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/msf/core/exploit/remote/ftp.rb', line 145

def recog_banner
  return nil unless banner
  return @recog_banner if instance_variable_defined?(:@recog_banner)

  @recog_banner = nil
  banner.to_s.each_line do |line|
    text = line.sub(/^\d{3}[\s-]/, '').strip
    next if text.empty?
    match = Recog::Nizer.match('ftp.banner', text)
    @recog_banner = match and break if match
  end
  @recog_banner
end

#recv_ftp_resp(nsock = self.sock) ⇒ Object

This method reads an FTP response based on FTP continuation stuff



517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
# File 'lib/msf/core/exploit/remote/ftp.rb', line 517

def recv_ftp_resp(nsock = self.sock)
  found_end = false
  resp = ""
  left = ""
  if !@ftpbuff.empty?
    left << @ftpbuff
    @ftpbuff = ""
  end

  unless left.empty?
    # Handle ends without newlines
    enlidx = left.rindex(0x0a.chr)
    tail = ''
    if enlidx != (left.length-1)
      if not enlidx
        tail = left
        left = ''
      else
        tail = left.slice!((enlidx+1)..left.length)
      end
    end

    rarr = left.split(/\r?\n/)
    left = tail
    rarr.each do |ln|
      if !found_end
        resp << ln << "\r\n"
        found_end = true if ln.match?(/\A\d{3} /)
      else
        @ftpbuff << ln << "\r\n"
      end
    end
    if found_end
      @ftpbuff << left
      print_status("FTP recv (buff): #{resp.inspect}") if datastore['FTPDEBUG']
      return resp
    end
  end

  while true
    data = nsock.get_once(-1, ftp_timeout)
    if not data
      @ftpbuff << resp
      @ftpbuff << left
      return data
    end

    got = left + data
    left = ""

    # handle the end w/o newline case
    enlidx = got.rindex(0x0a.chr)
    if enlidx != (got.length-1)
      if not enlidx
        left << got
        next
      else
        left << got.slice!((enlidx+1)..got.length)
      end
    end

    # split into lines
    rarr = got.split(/\r?\n/)
    rarr.each do |ln|
      if not found_end
        resp << ln
        resp << "\r\n"
        found_end = true if ln.match?(/\A\d{3} /)
      else
        left << ln
        left << "\r\n"
      end
    end
    if found_end
      print_status("FTP recv: #{resp.inspect}") if datastore['FTPDEBUG']
      print_status("FTP buff: #{left.inspect}") if datastore['FTPDEBUG'] && !left.empty?
      @ftpbuff << left
      return resp
    end
  end
end

#send_cmd(args, recv = true, nsock = self.sock) ⇒ Object

This method sends one command with zero or more parameters



432
433
434
435
436
437
438
439
# File 'lib/msf/core/exploit/remote/ftp.rb', line 432

def send_cmd(args, recv = true, nsock = self.sock)
  cmd = args.join(" ") + "\r\n"
  ret = raw_send(cmd, nsock)
  if (recv)
    return recv_ftp_resp(nsock)
  end
  return ret
end

#send_cmd_data(args, data, mode = 'a', nsock = self.sock) ⇒ Object

This method transmits the command in args and receives / uploads DATA via data channel For commands not needing data, it will fall through to the original send_cmd

For commands that send data only, the return will be the server response. For commands returning both data and a server response, an array will be returned.

NOTE: This function always waits for a response from the server.



450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
# File 'lib/msf/core/exploit/remote/ftp.rb', line 450

def send_cmd_data(args, data, mode = 'a', nsock = self.sock)
  type = nil
  # implement some aliases for various commands
  if (args[0] =~ /^DIR$/i || args[0] =~ /^LS$/i)
    # TODO || args[0] =~ /^MDIR$/i || args[0] =~ /^MLS$/i
    args[0] = "LIST"
    type = "get"
  elsif (args[0] =~ /^GET$/i)
    args[0] = "RETR"
    type = "get"
  elsif (args[0] =~ /^PUT$/i)
    args[0] = "STOR"
    type = "put"
  end

  # fall back if it's not a supported data command
  if not type
    return send_cmd(args, true, nsock)
  end

  # Set the transfer mode and connect to the remove server
  return nil if not data_connect(mode)

  # Our pending command should have got a connection now.
  res = send_cmd(args, true, nsock)
  # make sure could open port
  return nil unless res =~ /^(150|125) /

  # dispatch to the proper method
  if (type == "get")
    # failed listings just disconnect..
    begin
      data = datasocket.get(ftp_timeout, ftp_data_timeout)
    rescue ::EOFError
      data = nil
    end
  else
    sent = self.datasocket.put(data)
  end

  # close data channel so command channel updates
  data_disconnect

  # get status of transfer
  ret = nil
  if (type == "get")
    ret = recv_ftp_resp(nsock)
    ret = [ ret, data ]
  else
    ret = recv_ftp_resp(nsock)
  end

  ret
end

#send_pass(pass, nsock = self.sock) ⇒ Object

This method completes user authentication by sending the supplied password using the FTP 'PASS ' command.



416
417
418
419
# File 'lib/msf/core/exploit/remote/ftp.rb', line 416

def send_pass(pass, nsock = self.sock)
  raw_send("PASS #{pass}\r\n", nsock)
  recv_ftp_resp(nsock)
end

#send_quit(nsock = self.sock) ⇒ Object

This method sends a QUIT command.



424
425
426
427
# File 'lib/msf/core/exploit/remote/ftp.rb', line 424

def send_quit(nsock = self.sock)
  raw_send("QUIT\r\n", nsock)
  recv_ftp_resp(nsock)
end

#send_user(user, nsock = self.sock) ⇒ Object

This method logs in as the supplied user by transmitting the FTP 'USER ' command.



407
408
409
410
# File 'lib/msf/core/exploit/remote/ftp.rb', line 407

def send_user(user, nsock = self.sock)
  raw_send("USER #{user}\r\n", nsock)
  recv_ftp_resp(nsock)
end

#user ⇒ Object

Returns the user string from the 'FTPUSER' option.



616
617
618
# File 'lib/msf/core/exploit/remote/ftp.rb', line 616

def user
  datastore['FTPUSER']
end