description = [[ Attempts to guess valid credentials for the Citrix PN Web Agent XML Service.
The XML service authenticates against the local Windows server or the Active Directory.

CAUTION: This script makes no attempt of preventing account lockout. 
	     If the password list contains more passwords than the lockout-threshold
		 accounts WILL be locked.
]]

---
-- @usage
-- nmap --script=citrix-brute-xml --script-args=userdb=<userdb>,passdb=<passdb>,ntdomain=<domain> -p 80,443,8080 <host>
--
-- @output
-- PORT     STATE SERVICE    REASON
-- 8080/tcp open  http-proxy syn-ack
-- | citrix-brute-xml:  
-- |   Joe:password => Must change password at next logon
-- |   Luke:summer => Login was successful
-- |_  Jane:secret => Account is disabled
--
---

-- Version 0.2

-- Created 11/30/2009 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- Revised 12/02/2009 - v0.2 - Use stdnse.format_ouput for output


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

require 'unpwdb'
require 'shortport'
require 'citrixxml'

portrule = shortport.portnumber({8080,80,443}, "tcp")

--- Verifies if the credentials (username, password and domain) are valid
--
-- @param host string, the ip against which to perform
-- @param port number, the port number of the XML service
-- @param username string, the username to authenticate as
-- @param password string, the password to authenticate with
-- @param domain string, the Windows domain to authenticate against
--
-- @return success, message
--  
function verify_password( host, port, username, password, domain )

	local response = citrixxml.request_validate_credentials(host, port, {Credentials={Domain=domain, Password=password, UserName=username}})
	local cred_status = citrixxml.parse_validate_credentials_response(response)
	
	local account = {}

	account.username = username
	account.password = password
	account.domain = domain
			
	if cred_status.ErrorId then
		if cred_status.ErrorId == "must-change-credentials" then
			account.valid = true
			account.message = "Must change password at next logon"
		elseif cred_status.ErrorId == "account-disabled" then
			account.valid = true
			account.message = "Account is disabled"
		elseif cred_status.ErrorId == "account-locked-out" then
			account.valid = false
			account.message = "Account Locked Out"
		elseif cred_status.ErrorId == "failed-credentials" then
			account.valid = false
			account.message = "Incorrect Password"
		elseif cred_status.ErrorId == "unspecified" then
			account.valid = false
			account.message = "Unspecified"
		else
			print("UNKNOWN response: " .. response)
			account.valid = false
			account.message = "failed"
		end
	else
		account.message = "Login was successful"
		account.valid = true
	end

	return account
	
end

--- Formats the result from the table of valid accounts
--
-- @param accounts table containing accounts (tables)
-- @return string containing the result
function create_result_from_table(accounts)

	local result = ""

	for _, account in ipairs(accounts) do		
		result = result .. "  " .. account.username .. ":" .. account.password .. " => " .. account.message .. "\n"
	end
	
	return " \n" .. result
end

action = function(host, port)

	local status, nextUser, nextPass
	local username, password
	local args = nmap.registry.args
	local ntdomain = args.ntdomain
	local valid_accounts = {}
	
	if not ntdomain then
		return "FAILED: No domain specified (use ntdomain argument)"
	end
	
	status, nextUser = unpwdb.usernames()
	
	if not status then
		return
	end

	status, nextPass = unpwdb.passwords()

	if not status then
		return
	end
	
	username = nextUser()
	
	-- iterate over userlist
	while username do
		password = nextPass()
		
		-- iterate over passwordlist
		while password do
			local result = "Trying " .. username .. "/" .. password .. " "
			local account = verify_password(host.ip, port.number, username, password, ntdomain)
			
			if account.valid then
				
				table.insert(valid_accounts, account)
				
				if account.valid then
					stdnse.print_debug(1, "Trying %s/%s => Login Correct, Info: %s", username, password, account.message)
				else
					stdnse.print_debug(1, "Trying %s/%s => Login Correct", username, password)					
				end
			else
				stdnse.print_debug(1, "Trying %s/%s => Login Failed, Reason: %s", username, password, account.message)
			end
			password = nextPass()
		end
	
		nextPass("reset")
		username = nextUser()
	end
		
	return create_result_from_table(valid_accounts)
end