description = [[ Tries to get FTP login credentials by guessing usernames and passwords. This uses the standard unpwdb username/password list. However, in tests FTP servers are significantly slower than other servers when responding, so the number of usernames/passwords can be artificially limited using script arguments. ]] --- -- @output -- PORT STATE SERVICE REASON -- 21/tcp open ftp syn-ack -- | ftp-brute: -- | | anonymous: IEUser@ -- |_ |_ test: password -- -- @args userlimit The number of user accounts to try (default: unlimited). -- @args passlimit The number of passwords to try (default: unlimited). -- @args limit Set userlimlt and passlimit at the same time. -- 2008-11-06 Vlatko Kosturjak -- Modified xampp-default-auth script to generic ftp-brute script -- -- 2009-09-18 Ron Bowes -- Made into an actual bruteforce script (previously, it only tried one username/password). author = "Diman Todorov, Vlatko Kosturjak, Ron Bowes" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"auth", "intrusive"} require "shortport" require "stdnse" require "unpwdb" portrule = shortport.port_or_service(21, "ftp") local function get_limits() local userlimit = -1 local passlimit = -1 if(nmap.registry.args.userlimit) then userlimit = tonumber(nmap.registry.args.userlimit) end if(nmap.registry.args.passlimit) then passlimit = tonumber(nmap.registry.args.passlimit) end if(nmap.registry.args.limit) then userlimit = tonumber(nmap.registry.args.limit) passlimit = tonumber(nmap.registry.args.limit) end return userlimit, passlimit end local function login(host, port, user, pass) local status, err local res = "" -- Create a new socket local socket = nmap.new_socket() status, err = socket:connect(host, port) if(not(status)) then socket:close() return false, "Couldn't connect to host: " .. err end status, err = socket:send("USER " .. user .. "\r\n") if(not(status)) then socket:close() return false, "Couldn't send login: " .. err end status, err = socket:send("PASS " .. pass .. "\n\n") if(not(status)) then socket:close() return false, "Couldn't send login: " .. err end -- Create a buffer and receive the first line local buffer = stdnse.make_buffer(socket, "\r?\n") local line = buffer() -- Loop over the lines while(line)do stdnse.print_debug("Received: %s", line) if(string.match(line, "^230")) then stdnse.print_debug(1, "ftp-brute: Successful login: %s/%s", user, pass) socket:close() return true, true elseif(string.match(line, "^530")) then socket:close() return true, false elseif(string.match(line, "^220")) then elseif(string.match(line, "^331")) then else stdnse.print_debug(1, "ftp-brute: WARNING: Unhandled response: %s", line) end line = buffer() end socket:close() return false, "Login didn't return a proper response" end local function go(host, port) local status, err local result local userlimit, passlimit = get_limits() local authcombinations = { {user="anonymous", password="IEUser@"}, -- Anonymous user {user="nobody", password="xampp"} -- XAMPP default ftp } -- Load accounts from unpwdb local usernames, username, passwords, password -- Load the usernames status, usernames = unpwdb.usernames() if(not(status)) then return false, "Couldn't load username list: " .. usernames end -- Load the passwords status, passwords = unpwdb.passwords() if(not(status)) then return false, "Couldn't load password list: " .. usernames end -- Figure out how many local i = 0 local j = 0 -- Add the passwords to the authcombinations table password = passwords() while (password) do -- Limit the passwords i = i + 1 if(passlimit > 0 and i > passlimit) then break end j = 0 username = usernames() while(username) do -- Limit the usernames j = j + 1 if(userlimit > 0 and j > userlimit) then break end table.insert(authcombinations, {user=username, password=password}) username = usernames() end usernames('reset') password = passwords() end stdnse.print_debug(1, "ftp-brute: Loaded %d username/password pairs", #authcombinations) local results = {} for _, combination in ipairs(authcombinations) do -- Attempt a login status, result = login(host, port, combination.user, combination.password) -- Check for an error if(not(status)) then return false, result end -- Check for a success if(status and result) then table.insert(results, combination) end end return true, results end action = function(host, port) local response = {} local status, results = go(host, port) if(not(status)) then return stdnse.format_output(false, results) end if(#results == 0) then return stdnse.format_output(false, "No accounts found") end for i, v in ipairs(results) do table.insert(response, string.format("%s: %s\n", v.user, v.password)) end return stdnse.format_output(true, response) end