diff --git a/scripts/ftp-anon.nse b/scripts/ftp-anon.nse index 316b0c643..f2f81bb8c 100644 --- a/scripts/ftp-anon.nse +++ b/scripts/ftp-anon.nse @@ -4,9 +4,12 @@ Checks if an FTP server allows anonymous logins. --- -- @output --- |_ ftp-anon: Anonymous FTP login allowed +--- Default behavior +-- PORT STATE SERVICE +-- 21/tcp open ftp +-- |_ftp-anon: Anonymous FTP login allowed (FTP code 230) -author = "Eddie Bell" +author = "Eddie Bell, Rob Nicholls, Ange Gutek, David Fifield" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"default", "auth", "safe"} @@ -14,35 +17,103 @@ require "shortport" portrule = shortport.port_or_service(21, "ftp") +-- Read an FTP reply and return the numeric code and the message. See RFC 959, +-- section 4.2. The buffer argument should have been created with +-- stdnse.make_buffer(socket, "\r?\n"). On error, returns nil and an error +-- message. +local function read_reply(buffer) + local readline + local line, err + local code, message + local _, p, tmp + + line, err = buffer() + if not line then + return line, err + end + + -- Single-line response? + code, message = string.match(line, "^(%d%d%d) (.*)$") + if code then + return tonumber(code), message + end + + -- Multi-line response? + _, p, code, message = string.find(line, "^(%d%d%d)-(.*)$") + if p then + while true do + line, err = buffer() + if not line then + return line, err + end + tmp = string.match(line, "^%d%d%d (.*)$") + if tmp then + message = message .. "\n" .. tmp + break + end + message = message .. "\n" .. line + end + + return tonumber(code), message + end + + return nil, string.format("Unparseable response: %q", line) +end + + --- Connects to the FTP server and checks if the server allows anonymous logins. action = function(host, port) local socket = nmap.new_socket() - local result - local status = true - local isAnon = false - + local code, message local err_catch = function() socket:close() end local try = nmap.new_try(err_catch) - socket:set_timeout(5000) try(socket:connect(host.ip, port.number, port.protocol)) - try(socket:send("USER anonymous\r\n")) - try(socket:send("PASS IEUser@\r\n")) + buffer = stdnse.make_buffer(socket, "\r?\n") - while status do - status, result = socket:receive_lines(1); - if string.match(result, "^230") then - isAnon = true - break + -- Read banner. + code, message = read_reply(buffer) + if code and code == 220 then + try(socket:send("USER anonymous\r\n")) + code, message = read_reply(buffer) + if code == 331 then + -- 331: User name okay, need password. + try(socket:send("PASS IEUser@\r\n")) + code, message = read_reply(buffer) + end + + if code == 332 then + -- 332: Need account for login. + -- This is rarely seen but may come in response to a + -- USER or PASS command. As we're doing this + -- anonymously, send back a blank ACCT. + try(socket:send("ACCT\r\n")) + code, message = read_reply(buffer) + if code == 331 then + -- 331: User name okay, need password. + try(socket:send("PASS IEUser@\r\n")) + code, message = read_reply(buffer) + end end end socket:close() - if(isAnon) then - return "Anonymous FTP login allowed" + if code and code >= 200 and code < 300 then + -- We are primarily looking for 230: User logged in, proceed. + return "Anonymous FTP login allowed (FTP code " .. code .. ")" + elseif code == 421 then + -- 421: Service not available, closing control connection. + elseif code == 530 then + -- 530: Not logged in. + else + if not code then + stdnse.print_debug(1, "ftp-anon: got socket error %q.", message) + else + stdnse.print_debug(1, "ftp-anon: got code %d %q.", code, message) + end end end