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=,passdb=,ntdomain= -p 80,443,8080 -- -- @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 -- 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