description = [[
Checks if an FTP server allows anonymous logins.
Optionnaly with --script-args rw=test, checks whether it's readable or also writeable
]]

---
-- @output
--- Default behavior
-- PORT   STATE SERVICE
-- 21/tcp open  ftp
-- | ftp-anon: Anonymous FTP login allowed (FTP code 230)
-- |_(you can test Read/Write with --script-args rw=test)
--
--
-- Is writeable, but something occured when trying to clean our tracks
-- 21/tcp open  ftp
-- | ftp-anon: Anonymous FTP login allowed (FTP code 230) (Writeable)
-- |_/!\ WARNING : we may have left a directory behind us, unable to remove it ! (FTP code 500)
--
--
-- Is readable
-- 21/tcp open  ftp
-- |_ftp-anon: Anonymous FTP login allowed (FTP code 230) (Readable)

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"}

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 args = nmap.registry.args
	local isAnon = false
	local banner = true

	local err_catch = function()
		socket:close()
	end

	local try = nmap.new_try(err_catch)

	socket:set_timeout(30000)
	try(socket:connect(host.ip, port.number, port.protocol))
	buffer = stdnse.make_buffer(socket, "\r?\n")
	code, banner = read_reply(buffer)
	
	try(socket:send("USER anonymous\r\n"))
	code, message = read_reply(buffer)
	
while status do	
	
	if code == 331 then  -- Proceed with password, most common case
			socket:send("PASS IEUser@\r\n")
			code, message = read_reply(buffer)
						
	elseif code == 332 then -- this catches the rarely seen code that asks us to send an ACCT
			    -- as we're doing this anonymously, we'll just send back ACCT
			    try(socket:send("ACCT\r\n"))
			    code, message = read_reply(buffer)
			
	elseif code >= 200 and code < 300 then -- otherwise we want to check for a 2xx code, hopefully a 230
					      -- some servers will return 230 if they accept anonymous without a password
					      -- plus we're accepting anything that begins with a 2
					  status = false
					  isAnon = true
					  script_result = "Anonymous FTP login allowed (FTP code " .. code .. ")"
	else
	    status = false
	    script_result = "Unhandled answer received: " .. code .. "(" .. message .. ")"
	
	end
end
      
if isAnon and ( args['rw'] == 'test' ) then
	  math.randomseed(os.time())
	  local len_dir = math.random(3,8)
	  local letters_dir,name_dir
	  name_dir = string.char(math.random(65,90))
	  for i = 3,len_dir do
			    name_dir = name_dir .. string.char(math.random(65,90))
	  end
	  name_dir = name_dir .. string.char(math.random(65,90))

	  local dir_query = "MKD " .. name_dir .. "\r\n"

	  -- Send payload, 257 is "DIR created". If so, it's writeable. If not, just readable.
	  try(socket:send(dir_query))
	  code, message = read_reply(buffer)
	  if code == 257 then
			  script_result = script_result .. " (Writeable)"
			  -- gently remove our crap
			  dir_query = "RMD " .. name_dir .. "\r\n"
			  try(socket:send(dir_query))
			  code, message = read_reply(buffer)
			  if code >= 400 then
					  script_result = script_result .. "\n/!\\ WARNING : we may have left a directory behind us, unable to remove it ! (FTP code " .. code ..")"
			  end
	else
	  script_result = script_result .. " (Readable)"
	  end	
elseif isAnon and not args['rw'] then
				  script_result = script_result .. "\n(you can test Read/Write with --script-args rw=test)"
end
	  

socket:close()


return script_result
end
