summaryrefslogtreecommitdiffstats
path: root/scripts/ftp-anon.nse
blob: 50274b5474e7cc5d31c53957dee70013bb8499e6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
44
45
46
47
48
49
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
138
139
140
141
142
143
144
local ftp = require "ftp"
local match = require "match"
local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"

description = [[
Checks if an FTP server allows anonymous logins.

If anonymous is allowed, gets a directory listing of the root directory
and highlights writeable files.
]]

---
-- @see ftp-brute.nse
--
-- @args ftp-anon.maxlist The maximum number of files to return in the
-- directory listing. By default it is 20, or unlimited if verbosity is
-- enabled. Use a negative number to disable the limit, or
-- <code>0</code> to disable the listing entirely.
--
-- @output
-- PORT   STATE SERVICE
-- 21/tcp open  ftp
-- | ftp-anon: Anonymous FTP login allowed (FTP code 230)
-- | -rw-r--r--   1 1170     924            31 Mar 28  2001 .banner
-- | d--x--x--x   2 root     root         1024 Jan 14  2002 bin
-- | d--x--x--x   2 root     root         1024 Aug 10  1999 etc
-- | drwxr-srwt   2 1170     924          2048 Jul 19 18:48 incoming [NSE: writeable]
-- | d--x--x--x   2 root     root         1024 Jan 14  2002 lib
-- | drwxr-sr-x   2 1170     924          1024 Aug  5  2004 pub
-- |_Only 6 shown. Use --script-args ftp-anon.maxlist=-1 to see all.

author = {"Eddie Bell", "Rob Nicholls", "Ange Gutek", "David Fifield"}
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"default", "auth", "safe"}


portrule = shortport.port_or_service({21,990}, {"ftp","ftps"})

-- ---------------------
-- Directory listing function.
-- We ask for a PASV connexion, catch the port returned by the server, send a
-- LIST on the commands socket, connect to the data one and read the directory
-- list sent.
-- ---------------------
local function list(socket, buffer, target, max_lines)

  local list_socket, err = ftp.pasv(socket, buffer)
  if not list_socket then
    return nil, err
  end

  -- Send the LIST command on the commands socket. "Fire and forget"; we
  -- don't need to take care of the answer on this socket.
  local status, err = socket:send("LIST\r\n")
  if not status then
    return status, err
  end

  local listing = {}
  while not max_lines or #listing < max_lines do
    local status, data = list_socket:receive_buf(match.pattern_limit("\r?\n", 2048), false)
    if (not status and data == "EOF") or data == "" then
      break
    end
    if not status then
      return status, data
    end
    listing[#listing + 1] = data
  end

  return true, listing
end

--- Connects to the FTP server and checks if the server allows anonymous logins.
action = function(host, port)
  local max_list = stdnse.get_script_args("ftp-anon.maxlist")
  if not max_list then
    if nmap.verbosity() == 0 then
      max_list = 20
    else
      max_list = nil
    end
  else
    max_list = tonumber(max_list)
    if max_list < 0 then
      max_list = nil
    end
  end


  local socket, code, message, buffer = ftp.connect(host, port, {request_timeout=8000})
  if not socket then
    stdnse.debug1("Couldn't connect: %s", code or message)
    return nil
  end
  if code and code ~= 220 then
    stdnse.debug1("banner code %d %q.", code, message)
    return nil
  end

  local status, code, message = ftp.auth(socket, buffer, "anonymous", "IEUser@")
  if not status then
    if not code then
      stdnse.debug1("got socket error %q.", message)
    elseif code == 421 or code == 530 then
      -- Don't log known error codes.
      -- 421: Service not available, closing control connection.
      -- 530: Not logged in.
    else
      stdnse.debug1("got code %d %q.", code, message)
      return ("got code %d %q."):format(code, message)
    end
    return nil
  end

  local result = {}
  result[#result + 1] = "Anonymous FTP login allowed (FTP code " .. code .. ")"

  if not max_list or max_list > 0 then
    local status, listing = list(socket, buffer, host, max_list)
    ftp.close(socket)

    if not status then
      result[#result + 1] = "Can't get directory listing: " .. listing
    else
      for _, item in ipairs(listing) do
        -- Just a quick passive check on user rights.
        if string.match(item, "^[d-].......w.") then
          item = item .. " [NSE: writeable]"
        end
        result[#result + 1] = item
      end
      if max_list and #listing == max_list then
        result[#result + 1] = string.format("Only %d shown. Use --script-args %s.maxlist=-1 to see all.", #listing, SCRIPT_NAME)
      end
    end
  end

  return table.concat(result, "\n")
end