description = [[
Performs password guessing against Apple Filing Protocol (AFP).
]]

---
-- @usage 
-- nmap -p 548 --script afp-brute <host>
--
-- @output
-- PORT    STATE SERVICE
-- 548/tcp open  afp
-- | afp-brute:  
-- |_  admin:KenSentMe => Valid credentials

-- Information on AFP implementations
--
-- Snow Leopard
-- ------------
-- - Delay 10 seconds for accounts with more than 5 incorrect login attempts (good)
-- - Instant response if password is successfull
--
-- Netatalk
-- --------
-- - Netatalk responds with a "Parameter error" when the username is invalid
--

author = "Patrik Karlsson"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"intrusive", "brute"}

require 'shortport'
require 'stdnse'
require 'afp'
require 'unpwdb'

-- Version 0.3
-- Created 01/15/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- Revised 03/09/2010 - v0.2 - changed so that passwords are iterated over users
--                           - this change makes better sence as guessing is slow
-- Revised 09/09/2011 - v0.3 - changed account status text to be more consistent with other *-brute scripts

portrule = shortport.port_or_service(548, "afp")

action = function( host, port )

	local result, response, status = {}, nil, nil
	local valid_accounts, found_users = {}, {}
	local helper
	
 	status, usernames = unpwdb.usernames()
	if not status then return end

	status, passwords = unpwdb.passwords()	
	if not status then return end
	
	for password in passwords do
		for username in usernames do
			if ( not(found_users[username]) ) then

				helper = afp.Helper:new()
				status, response = helper:OpenSession( host, port )

				if ( not(status) ) then
					stdnse.print_debug("OpenSession failed")
					return
				end


				stdnse.print_debug( string.format("Trying %s/%s ...", username, password ) )
				status, response = helper:Login( username, password )

				-- if the response is "Parameter error." we're dealing with Netatalk
				-- This basically means that the user account does not exist
				-- In this case, why bother continuing? Simply abort and thank Netatalk for the fish
				if response:match("Parameter error.") then
					stdnse.print_debug("Netatalk told us the user does not exist! Thanks.")
					-- mark it as "found" to skip it
					found_users[username] = true
				end

				if status then				
					-- Add credentials for other afp scripts to use
					if nmap.registry.afp == nil then
						nmap.registry.afp = {}
					end	
					nmap.registry.afp[username]=password
					found_users[username] = true

					table.insert( valid_accounts, string.format("%s:%s => Valid credentials", username, password:len()>0 and password or "<empty>" ) )
					break
				end
				helper:CloseSession()
			end
		end	
		usernames("reset")
	end
	
	local output = stdnse.format_output(true, valid_accounts)
	
	return output
	
end
