diff --git a/CHANGELOG b/CHANGELOG index f8104c23d..dccfb946e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,17 @@ o Added new NSE scripts: system, server build date, and upstream time server IP address. [Richard Sammet] + o citrix-brute-xml uses the unpwdb library to guess credentials for + the Citrix PN Web Agent Service. [Patrik Karlsson] + + o citrix-enum-apps and citrix-enum-apps-xml get a list of published + applications from the Citrix ICA Browser or XML service, + respectively. [Patrik Karlsson] + + o citrix-enum-servers and citrix-enum-servers-xml.nse get a list of + Citrix servers from the Citrix ICA Browser or XML service, + respectively. [Patrik Karlsson] + o Removed a limitation of snmp.lua that only allowed it to properly encode OID component values up to 127. The bug was reported by Victor Rudnev. [David] diff --git a/nselib/citrixxml.lua b/nselib/citrixxml.lua new file mode 100644 index 000000000..0fd8bd60b --- /dev/null +++ b/nselib/citrixxml.lua @@ -0,0 +1,571 @@ +--- +-- This module was written by Patrik Karlsson and facilitates communication +-- with the Citrix XML Service. It is not feature complete and is missing several +-- functions and parameters. +-- +-- The library makes little or no effort to verify that the parameters submitted +-- to each function are compliant with the DTD +-- +-- As all functions handling requests take their parameters in the form of tables, +-- additional functionality can be added while not breaking existing scripts +-- +-- Details regarding the requests/responses and their parameters can be found in +-- the NFuse.DTD included with Citrix MetaFrame/Xenapp +-- +-- This code is based on the information available in: +-- NFuse.DTD - Version 5.0 (draft 1) 24 January 2008 +-- + + +require 'http' + +module(... or "citrix",package.seeall) + +--- Decodes html-entities to chars eg. => +-- +-- @param str string to convert +-- @return string an e +function decode_xml_document(xmldata) + + local hexval + + if not xmldata then + return "" + end + + local newstr = xmldata + + for m in xmldata:gmatch("(\&\#%d+;)") do + hexval = m:match("(%d+)") + + if ( hexval ) then + newstr = xmldata:gsub(m, string.char(hexval)) + end + end + + return newstr + +end + +--- Sends the request to the server using the http lib +-- +-- NOTE: +-- At the time of the development (20091128) the http +-- lib does not properly handle text/xml content. It also doesn't +-- handle HTTP 100 Continue properly. Workarounds are in place, +-- please consult comments. +-- +-- @param host string, the ip of the remote server +-- @param port number, the port of the remote server +-- @param xmldata string, the HTTP data part of the request as XML +-- +-- @return string with the response body +-- +function send_citrix_xml_request(host, port, xmldata) + + local header = "POST /scripts/WPnBr.dll HTTP/1.1\r\n" + header = header .. "Content-type: text/xml\r\n" + header = header .. "Host: " .. host .. ":" .. port .. "\r\n" + header = header .. "Content-Length: " .. xmldata:len() .. "\r\n" + header = header .. "Connection: Close\r\n" + header = header .. "\r\n" + + local request = header .. xmldata + + -- this would have been really great! Unfortunately buildPost substitutes all spaces for plus' + -- this ain't all great when the content-type is text/xml + -- local response = http.post( host, port, "/scripts/WPnBr.dll", { header={["Content-Type"]="text/xml"}}, nil, xmldata) + + -- let's build the content ourselves and let the http module do the rest + local response = http.request(host, port, request) + local parse_options = {method="post"} + + -- we need to handle another bug within the http module + -- it doesn't seem to recognize the HTTP/100 Continue correctly + -- So, we need to chop that part of from the response + if response and response:match("^HTTP/1.1 100 Continue") and response:match( "\r?\n\r?\n" ) then + response = response:match( "\r?\n\r?\n(.*)$" ) + end + + -- time for next workaround + -- The Citrix XML Service returns the header Transfer-Coding, rather than Transfer-Encoding + -- Needless to say, this screws things up for the http library + if response and response:match("Transfer[-]Coding") then + response = response:gsub("Transfer[-]Coding", "Transfer-Encoding") + end + + local response = http.parseResult(response, parse_options) + + -- this is *probably* not the right way to do stuff + -- decoding should *probably* only be done on XML-values + -- this is *probably* defined in the standard, for anyone interested + return decode_xml_document(response.body) + +end + +--- Request information about the Citrix Server Farm +-- +-- Consult the NFuse.DTD for a complete list of supported parameters +-- This function implements all the supported parameters described in: +-- Version 5.0 (draft 1) 24 January 2008 +-- +-- @param socket socket, connected to the remote web server +-- @return string HTTP response data +-- +function request_server_farm_data( host, port ) + + local xmldata = "\r\n" + xmldata = xmldata .. "\r\n" + xmldata = xmldata .. "" + xmldata = xmldata .. "" + xmldata = xmldata .. "\r\n" + + return send_citrix_xml_request(host, port, xmldata) +end + +--- Parses the response from the request_server_farm_data request +-- @param response string with the XML response +-- @return table containing server farm names +-- +function parse_server_farm_data_response( response ) + + local farms = {} + + response = response:gsub("\r?\n","") + for farm in response:gmatch("([^\<]+)") do + table.insert(farms, farm) + end + + return farms + +end + +--- Sends a request for application data to the Citrix XML service +-- +-- Consult the NFuse.DTD for a complete list of supported parameters +-- This function does NOT implement all the supported parameters +-- +-- Supported parameters are Scope, ServerType, ClientType, DesiredDetails +-- +-- @param host string the host which is to be queried +-- @param port number the port number of the XML service +-- @param params table with parameters +-- @return string HTTP response data +-- +function request_appdata(host, port, params) + + -- setup the mandatory parameters if they're missing + local scope = params['Scope'] or "onelevel" + local server_type = params['ServerType'] or "all" + local client_type = params['ClientType'] or "ica30" + local desired_details = params['DesiredDetails'] or nil + + local xmldata = "\r\n" + xmldata = xmldata .. "\r\n" + xmldata = xmldata .. "" + xmldata = xmldata .. "" + xmldata = xmldata .. "" + xmldata = xmldata .. "" .. server_type .. "" + xmldata = xmldata .. "" .. client_type .. "" + + if desired_details then + if type(desired_details) == "string" then + xmldata = xmldata .. "" .. desired_details .. "" + elseif type(desired_details) == "table" then + for _, v in ipairs(desired_details) do + xmldata = xmldata .. "" .. v .. "" + end + else + assert(desired_details) + end + + end + + xmldata = xmldata .. "" + xmldata = xmldata .. "\r\n" + + return send_citrix_xml_request(host, port, xmldata) +end + + +--- Extracts the Accesslist section of the XML response +-- +-- @param xmldata string containing results from the request app data request +-- @return table containing settings extracted from the accesslist section of the response +local function extract_appdata_acls(xmldata) + + local acls = {} + local users = {} + local groups = {} + + for acl in xmldata:gmatch("(.-)") do + + if acl:match("AnonymousUser") then + table.insert(users, "Anonymous") + else + + for user in acl:gmatch("(.-)") do + local user_name = user:match("(.-)") or "" + local domain_name = user:match("(.-)") or "" + + if user_name:len() > 0 then + if domain_name:len() > 0 then + domain_name = domain_name .. "\\" + end + table.insert(users, domain_name .. user_name) + end + + end + + for group in acl:gmatch("(.-)") do + + + local group_name = group:match("(.-)") or "" + local domain_name = group:match("(.-)") or "" + + if group_name:len() > 0 then + if domain_name:len() > 0 then + domain_name = domain_name .. "\\" + end + table.insert(groups, domain_name .. group_name) + end + + end + + end + + if #users> 0 then + acls['User'] = users + end + if #groups>0 then + acls['Group'] = groups + end + + end + + return acls + +end + + +--- Extracts the settings section of the XML response +-- +-- @param xmldata string containing results from the request app data request +-- @return table containing settings extracted from the settings section of the response +local function extract_appdata_settings(xmldata) + + local settings = {} + + settings['appisdisabled'] = xmldata:match("") + settings['appisdesktop'] = xmldata:match("") + + for s in xmldata:gmatch("(.-)") do + settings['Encryption'] = s:match("(.-)") + settings['AppOnDesktop'] = s:match("") + settings['AppInStartmenu'] = s:match("") + settings['PublisherName'] = s:match("(.-)") + settings['SSLEnabled'] = s:match("(.-)") + settings['RemoteAccessEnabled'] = s:match("(.-)") + end + + return settings + +end + +--- Parses the appdata XML response +-- +-- @param xmldata string response from request_appdata +-- @return table containing nestled tables closely resembling the DOM model of the XML response +function parse_appdata_response(xmldata) + + local apps = {} + xmldata = xmldata:gsub("\r?\n",""):gsub(">%s+<", "><") + + for AppData in xmldata:gmatch("(.-)") do + + local app_name = AppData:match("(.-)") or "" + local app = {} + + app['FName'] = app_name + app['AccessList'] = extract_appdata_acls(AppData) + app['Settings'] = extract_appdata_settings(AppData) + + table.insert(apps, app) + + end + + return apps +end + +-- +-- +-- @param flags string, should be any of following: alt-addr, no-load-bias +-- +function request_address(host, port, flags, appname) + + local xmldata = "\r\n" + xmldata = xmldata .. "\r\n" + xmldata = xmldata .. "" + xmldata = xmldata .. "" + + if flags then + xmldata = xmldata .. "" .. flags .. "" + end + + if appname then + xmldata = xmldata .. "" + xmldata = xmldata .. "" .. appname .. "" + xmldata = xmldata .. "" + end + + xmldata = xmldata .. "" + xmldata = xmldata .. "\r\n" + + return send_citrix_xml_request(host, port, xmldata) +end + +--- Request information about the Citrix protocol +-- +-- Consult the NFuse.DTD for a complete list of supported parameters +-- This function implements all the supported parameters described in: +-- Version 5.0 (draft 1) 24 January 2008 +-- +-- @param host string the host which is to be queried +-- @param port number the port number of the XML service +-- @param params table with parameters +-- @return string HTTP response data +-- +function request_server_data(host, port, params) + + local params = params or {} + local server_type = params.ServerType or {"all"} + local client_type = params.ClientType or {"all"} + + local xmldata = "\r\n" + xmldata = xmldata .. "\r\n" + xmldata = xmldata .. "" + xmldata = xmldata .. "" + + for _, srvtype in pairs(server_type) do + xmldata = xmldata .. "" .. srvtype .. "" + end + + for _, clitype in pairs(client_type) do + xmldata = xmldata .. "" .. clitype .. "" + end + + xmldata = xmldata .. "" + xmldata = xmldata .. "\r\n" + + return send_citrix_xml_request(host, port, xmldata) +end + +--- Parses the response from the request_server_data request +-- @param response string with the XML response +-- @return table containing the server names +-- +function parse_server_data_response(response) + + local servers = {} + + response = response:gsub("\r?\n","") + for s in response:gmatch("([^\<]+)") do + table.insert(servers, s) + end + + return servers + +end + +--- Request information about the Citrix protocol +-- +-- Consult the NFuse.DTD for a complete list of supported parameters +-- This function implements all the supported parameters described in: +-- Version 5.0 (draft 1) 24 January 2008 +-- +-- @param host string the host which is to be queried +-- @param port number the port number of the XML service +-- @param params table with parameters +-- @return string HTTP response data +-- +function request_protocol_info( host, port, params ) + + local params = params or {} + + local xmldata = "\r\n" + xmldata = xmldata .. "\r\n" + xmldata = xmldata .. "" + xmldata = xmldata .. "" + + if params['ServerAddress'] then + xmldata = xmldata .. "" + xmldata = xmldata .. params['ServerAddress'] .. "" + end + + xmldata = xmldata .. "" + xmldata = xmldata .. "\r\n" + + return send_citrix_xml_request(host, port, xmldata) +end + +--- Request capability information +-- +-- Consult the NFuse.DTD for a complete list of supported parameters +-- This function implements all the supported parameters described in: +-- Version 5.0 (draft 1) 24 January 2008 +-- +-- @param host string the host which is to be queried +-- @param port number the port number of the XML service +-- @param params table with parameters +-- @return string HTTP response data +-- +function request_capabilities( host, port ) + + local xmldata = "\r\n" + xmldata = xmldata .. "\r\n" + xmldata = xmldata .. "" + xmldata = xmldata .. "" + xmldata = xmldata .. "" + xmldata = xmldata .. "\r\n" + + return send_citrix_xml_request(host, port, xmldata) +end + +--- Parses the response from the request_capabilities request +-- @param response string with the XML response +-- @return table containing the server capabilities +-- +function parse_capabilities_response(response) + + local servers = {} + + response = response:gsub("\r?\n","") + for s in response:gmatch("([^\<]+)") do + table.insert(servers, s) + end + + return servers + +end + +--- Tries to validate user credentials against the XML service +-- +-- Consult the NFuse.DTD for a complete list of supported parameters +-- This function implements all the supported parameters described in: +-- Version 5.0 (draft 1) 24 January 2008 +-- +-- +-- @param host string the host which is to be queried +-- @param port number the port number of the XML service +-- @param params table with parameters +-- @return string HTTP response data +-- +function request_validate_credentials(host, port, params ) + + local params = params or {} + local credentials = params['Credentials'] or {} + + local xmldata = "\r\n" + xmldata = xmldata .. "\r\n" + xmldata = xmldata .. "" + xmldata = xmldata .. "" + xmldata = xmldata .. "" + + if credentials['UserName'] then + xmldata = xmldata .. "" .. credentials['UserName'] .. "" + end + + if credentials['Password'] then + xmldata = xmldata .. "" .. credentials['Password'] .. "" + end + + if credentials['Domain'] then + xmldata = xmldata .. "" .. credentials['Domain'] .. "" + end + + xmldata = xmldata .. "" + xmldata = xmldata .. "" + xmldata = xmldata .. "\r\n" + + return send_citrix_xml_request(host, port, xmldata) + +end + + +--- Parses the response from request_validate_credentials +-- @param response string with the XML response +-- @return table containing the results +-- +function parse_validate_credentials_response(response) + local tblResult = {} + + response = response:gsub("\r?\n","") + tblResult['DaysUntilPasswordExpiry'] = response:match("(.+)") + tblResult['ShowPasswordExpiryWarning'] = response:match("(.+)") + tblResult['ErrorId'] = response:match("(.+)") + + return tblResult + +end + +--- Sends a request to reconnect session data +-- +-- Consult the NFuse.DTD for a complete list of supported parameters +-- This function does NOT implement all the supported parameters +---- +-- @param host string the host which is to be queried +-- @param port number the port number of the XML service +-- @param params table with parameters +-- +function request_reconnect_session_data(host, port, params) + + local params = params or {} + local Credentials = params.Credentials or {} + + params.ServerType = params.ServerType or {} + params.ClientType = params.ClientType or {} + + local xmldata = "\r\n" + xmldata = xmldata .. "\r\n" + xmldata = xmldata .. "" + xmldata = xmldata .. "" + + xmldata = xmldata .. "" + + if Credentials.UserName then + xmldata = xmldata .. "" .. Credentials.UserName .. "" + end + + if Credentials.Password then + xmldata = xmldata .. "" .. Credentials.Password .. "" + end + + if Credentials.Domain then + xmldata = xmldata .. "" .. Credentials.Domain .. "" + end + + xmldata = xmldata .. "" + + if params.ClientName then + xmldata = xmldata .. "" .. params.ClientName .. "" + end + + if params.DeviceId then + xmldata = xmldata .. "" .. params.DeviceId .. "" + end + + for _, srvtype in pairs(params.ServerType) do + xmldata = xmldata .. "" .. srvtype .. "" + end + + for _, clitype in pairs(params.ClientType) do + xmldata = xmldata .. "" .. clitype .. "" + end + + xmldata = xmldata .. "" + xmldata = xmldata .. "\r\n" + + return send_citrix_xml_request(host, port, xmldata) + + +end diff --git a/scripts/citrix-brute-xml.nse b/scripts/citrix-brute-xml.nse new file mode 100644 index 000000000..073989166 --- /dev/null +++ b/scripts/citrix-brute-xml.nse @@ -0,0 +1,160 @@ +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 \ No newline at end of file diff --git a/scripts/citrix-enum-apps-xml.nse b/scripts/citrix-enum-apps-xml.nse new file mode 100644 index 000000000..522eb13cf --- /dev/null +++ b/scripts/citrix-enum-apps-xml.nse @@ -0,0 +1,150 @@ +description = [[ +Extracts a list of applications, acls and settings from Citrix XML service + +The script returns the shorter, comma separated output per default. +Running nmap with the verbose flag (-v) triggers the detailed output. +]] + +--- +-- @usage +-- nmap --script=citrix-enum-apps-xml -p 80,443,8080 +-- +-- @output +-- PORT STATE SERVICE +-- 8080/tcp open http-proxy +-- | citrix-enum-apps-xml: +-- | Application: Notepad +-- | Disabled: false +-- | Desktop: false +-- | On Desktop: false +-- | Encryption: basic +-- | In start menu: false +-- | Publisher: labb1farm +-- | SSL: false +-- | Remote Access: false +-- | Users: Anonymous +-- | Application: iexplorer +-- | Disabled: false +-- | Desktop: false +-- | On Desktop: false +-- | Encryption: basic +-- | In start menu: false +-- | Publisher: labb1farm +-- | SSL: false +-- | Remote Access: false +-- | Users: Anonymous +-- | Application: registry editor +-- | Disabled: false +-- | Desktop: false +-- | On Desktop: false +-- | Encryption: basic +-- | In start menu: false +-- | Publisher: labb1farm +-- | SSL: false +-- | Remote Access: false +-- | Users: WIN-B4RL0SUCJ29\Joe +-- |_ Groups: WIN-B4RL0SUCJ29\HR, *CITRIX_BUILTIN*\*CITRIX_ADMINISTRATORS* +-- +-- +-- PORT STATE SERVICE +-- 8080/tcp open http-proxy +-- | citrix-enum-apps-xml: +-- | Application: Notepad; Users: Anonymous +-- | Application: iexplorer; Users: Anonymous +-- |_ Application: registry editor; Users: WIN-B4RL0SUCJ29\Joe; Groups: WIN-B4RL0SUCJ29\HR, *CITRIX_BUILTIN*\*CITRIX_ADMINISTRATORS* +-- +--- + +-- Version 0.2 +-- Created 11/26/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 = {"discovery", "safe"} + +require "comm" +require 'shortport' +require 'citrixxml' + +portrule = shortport.portnumber({8080,80,443}, "tcp") + +--- Creates a table which is suitable for use with stdnse.format_output +-- +-- @param appdata table with results from parse_appdata_response +-- @param mode string short or long, see usage above +-- @return table suitable for stdnse.format_output +function format_output(appdata, mode) + + local result = {} + local setting_titles = { {appisdisabled="Disabled"}, {appisdesktop="Desktop"}, {AppOnDesktop="On Desktop"}, + {Encryption="Encryption"}, {AppInStartmenu="In start menu"}, + {PublisherName="Publisher"}, {SSLEnabled="SSL"}, {RemoteAccessEnabled="Remote Access"} } + + + if mode == "short" then + for app_name, AppData in ipairs(appdata) do + local line = "Application: " .. AppData.FName + + if AppData.AccessList then + + if AppData.AccessList.User then + line = line .. "; Users: " .. stdnse.strjoin(", ", AppData.AccessList.User) + end + + if AppData.AccessList.Group then + line = line .. "; Groups: " .. stdnse.strjoin(", ", AppData.AccessList.Group) + end + + table.insert(result, line) + end + end + + else + + for app_name, AppData in ipairs(appdata) do + local result_part = {} + + result_part.name = "Application: " .. AppData.FName + + local settings = AppData.Settings + + for _, setting_pairs in ipairs(setting_titles) do + for setting_key, setting_title in pairs(setting_pairs) do + local setting_value = settings[setting_key] and settings[setting_key] or "" + table.insert(result_part, setting_title .. ": " .. setting_value ) + end + end + + + if AppData.AccessList then + if AppData.AccessList.User then + table.insert(result_part, "Users: " .. stdnse.strjoin(", ", AppData.AccessList.User) ) + end + + if AppData.AccessList.Group then + table.insert(result_part, "Groups: " .. stdnse.strjoin(", ", AppData.AccessList.Group) ) + end + + table.insert(result, result_part) + end + + end + + end + + return result + + end + + +action = function(host,port) + + local response = citrixxml.request_appdata(host.ip, port.number, {ServerAddress="",attr={addresstype="dot"},DesiredDetails={"all","access-list"} }) + local appdata = citrixxml.parse_appdata_response(response) + + local response = format_output(appdata, (nmap.verbosity() > 1 and "long" or "short")) + + return stdnse.format_output(true, response) + +end \ No newline at end of file diff --git a/scripts/citrix-enum-apps.nse b/scripts/citrix-enum-apps.nse new file mode 100644 index 000000000..152f34199 --- /dev/null +++ b/scripts/citrix-enum-apps.nse @@ -0,0 +1,157 @@ +description = [[ +Extract published applications from the ICA Browser service +]] + +--- +-- @usage sudo ./nmap -sU --script=citrix-enum-apps -p 1604 +-- +-- @output +-- PORT STATE SERVICE +-- 1604/udp open unknown +-- 1604/udp open unknown +-- | citrix-enum-apps: +-- | Notepad +-- | iexplorer +-- |_ registry editor +-- + +-- Version 0.2 + +-- Created 11/24/2009 - v0.1 - created by Patrik Karlsson +-- Revised 11/25/2009 - v0.2 - fixed multiple packet response bug + +author = "Patrik Karlsson " + +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" + +categories = {"discovery","intrusive"} + +require "comm" +require "shortport" +require "stdnse" +require "bin" + +portrule = shortport.portnumber(1604, "udp") + + +-- process the response from the server +-- @param response string, complete server response +-- @return string row delimited with \n containing all published applications +function process_pa_response(response) + + local pos, packet_len = bin.unpack("SS", response) + local app_name + local pa_list = {} + + if packet_len < 40 then + return + end + + -- the list of published applications starts at offset 40 + offset = 41 + + while offset < packet_len do + pos, app_name = bin.unpack("z", response:sub(offset)) + offset = offset + pos - 1 + + table.insert(pa_list, app_name) + end + + return pa_list + +end + + +action = function(host, port) + + local packet, counter + local query = {} + local pa_list = {} + + -- + -- Packets were intercepted from the Citrix Program Neighborhood client + -- They are used to query a server for it's list of servers + -- + -- We're really not interested in the responses to the first two packets + -- The third response contains the list of published applications + -- I couldn't find any documentation on this protocol so I'm providing + -- some brief information for the bits and bytes this script uses. + -- + -- Spec. of response to query[2] that contains a list of published apps + -- + -- offset size content + -- ------------------------- + -- 0 16-bit Length + -- 12 32-bit Server IP (not used here) + -- 30 8-bit Last packet (1), More packets(0) + -- 40 - null-separated list of applications + -- + query[0] = string.char( + 0x1e, 0x00, -- Length: 30 + 0x01, 0x30, 0x02, 0xfd, 0xa8, 0xe3, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + ) + + query[1] = string.char( + 0x20, 0x00, -- Length: 32 + 0x01, 0x36, 0x02, 0xfd, 0xa8, 0xe3, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ) + + query[2] = string.char( + 0x2a, 0x00, -- Length: 42 + 0x01, 0x32, 0x02, 0xfd, 0xa8, 0xe3, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x21, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ) + + counter = 0 + + local socket = nmap.new_socket() + socket:set_timeout(5000) + + try = nmap.new_try(function() socket:close() end) + + try( socket:connect(host.ip, port.number, port.protocol) ) + + -- send the two first packets and never look back + repeat + try( socket:send(query[counter]) ) + packet = try(socket:receive()) + counter = counter + 1 + until (counter>#query) + + -- process the first response + pa_list = process_pa_response( packet ) + + -- + -- the byte at offset 31 in the response has a really magic function + -- if it is set to zero (0) we have more response packets to process + -- if it is set to one (1) we have arrived at the last packet of our journey + -- + while packet:sub(31,31) ~= string.char(0x01) do + packet = try( socket:receive() ) + local tmp_table = process_pa_response( packet ) + + for _,v in pairs(tmp_table) do + table.insert(pa_list, v) + end + + end + + -- set port to open + if #pa_list>0 then + nmap.set_port_state(host, port, "open") + end + + socket:close() + + return stdnse.format_output(true, pa_list) + +end \ No newline at end of file diff --git a/scripts/citrix-enum-servers-xml.nse b/scripts/citrix-enum-servers-xml.nse new file mode 100644 index 000000000..a0f05f498 --- /dev/null +++ b/scripts/citrix-enum-servers-xml.nse @@ -0,0 +1,45 @@ +description = [[ Extracts the name of the server farm and member severs from Citrix XML service +]] + +--- +-- @usage +-- nmap --script=citrix-enum-servers-xml -p 80,443,8080 +-- +-- @output +-- PORT STATE SERVICE REASON +-- 8080/tcp open http-proxy syn-ack +-- | citrix-enum-servers-xml: +-- | CITRIX-SRV01 +-- |_ CITRIX-SRV01 +-- +--- + +-- Version 0.2 + +-- Created 11/26/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 = {"discovery", "safe"} + +require "comm" +require 'shortport' +require 'citrixxml' + +portrule = shortport.portnumber({8080,80,443}, "tcp") + + +action = function(host, port) + + local xmldata = citrixxml.request_server_data(host.ip, port.number) + local servers = citrixxml.parse_server_data_response(xmldata) + local response = {} + + for _, srv in ipairs(servers) do + table.insert(response, srv) + end + + return stdnse.format_output(true, response) + +end diff --git a/scripts/citrix-enum-servers.nse b/scripts/citrix-enum-servers.nse new file mode 100644 index 000000000..352ed13d9 --- /dev/null +++ b/scripts/citrix-enum-servers.nse @@ -0,0 +1,141 @@ +description = [[ +Extract a list of Citrix servers from the ICA Browser service +]] + +--- +-- @usage sudo ./nmap -sU --script=citrix-enum-servers -p 1604 +-- +-- @output +-- PORT STATE SERVICE +-- 1604/udp open unknown +-- | citrix-enum-servers: +-- | CITRIXSRV01 +-- |_ CITRIXSRV02 +-- + +-- Version 0.2 + +-- Created 11/26/2009 - v0.1 - created by Patrik Karlsson +-- Revised 11/26/2009 - v0.2 - minor packet documentation + + +author = "Patrik Karlsson " +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +require "comm" +require "shortport" + +portrule = shortport.portnumber(1604, "udp") + +-- +-- process the response from the server +-- @param response string, complete server response +-- @return string row delimited with \n containing all published applications +-- +function process_server_response(response) + + local pos, packet_len = bin.unpack("SS", response) + local server_name + local server_list = {} + + if packet_len < 40 then + return + end + + -- the list of published applications starts at offset 40 + offset = 41 + + while offset < packet_len do + pos, server_name = bin.unpack("z", response:sub(offset)) + offset = offset + pos - 1 + table.insert(server_list, server_name) + end + + return server_list + +end + + +action = function(host, port) + + local packet, counter, socket + local query = {} + local server_list = {} + + -- + -- Packets were intercepted from the Citrix Program Neighborhood client + -- They are used to query a server for it's list of published applications + -- + -- We're really not interested in the responses to the first two packets + -- The third response contains the list of published applications + -- I couldn't find any documentation on this protocol so I'm providing + -- some brief information for the bits and bytes this script uses. + -- + -- Spec. of response to query[2] that contains a list of published apps + -- + -- offset size content + -- ------------------------- + -- 0 16-bit Length + -- 12 32-bit Server IP (not used here) + -- 30 8-bit Last packet (1), More packets(0) + -- 40 - null-separated list of applications + -- + query[0] = string.char( + 0x1e, 0x00, -- Length: 30 + 0x01, 0x30, 0x02, 0xfd, 0xa8, 0xe3, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + ) + + query[1] = string.char( + 0x2a, 0x00, -- Length: 42 + 0x01, 0x32, 0x02, 0xfd, 0xa8, 0xe3, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ) + + counter = 0 + + socket = nmap.new_socket() + socket:set_timeout(5000) + + try = nmap.new_try(function() socket:close() end) + try(socket:connect(host.ip, port.number, port.protocol)) + + -- send the two first packets and never look back + repeat + try(socket:send(query[counter])) + packet = try(socket:receive()) + counter = counter + 1 + until (counter>#query) + + -- process the first response + server_list = process_server_response( packet ) + + -- + -- the byte at offset 31 in the response has a really magic function + -- if it is set to zero (0) we have more response packets to process + -- if it is set to one (1) we have arrived at the last packet of our journey + -- + while packet:sub(31,31) ~= string.char(0x01) do + packet = try( socket:receive() ) + local tmp_table = process_server_response( packet ) + + for _, v in ipairs(tmp_table) do + table.insert(server_list, v) + end + end + + if #server_list>0 then + nmap.set_port_state(host, port, "open") + end + + socket:close() + + return stdnse.format_output(true, server_list) + +end \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index 7fec156cc..53764fe5a 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -2,6 +2,11 @@ Entry { filename = "asn-query.nse", categories = { "discovery", "external", "saf Entry { filename = "auth-owners.nse", categories = { "default", "safe", } } Entry { filename = "auth-spoof.nse", categories = { "malware", "safe", } } Entry { filename = "banner.nse", categories = { "discovery", "safe", } } +Entry { filename = "citrix-brute-xml.nse", categories = { "auth", "intrusive", } } +Entry { filename = "citrix-enum-apps-xml.nse", categories = { "discovery", "safe", } } +Entry { filename = "citrix-enum-apps.nse", categories = { "discovery", "intrusive", } } +Entry { filename = "citrix-enum-servers-xml.nse", categories = { "discovery", "safe", } } +Entry { filename = "citrix-enum-servers.nse", categories = { "discovery", "safe", } } Entry { filename = "daytime.nse", categories = { "discovery", "safe", } } Entry { filename = "db2-info.nse", categories = { "discovery", "safe", "version", } } Entry { filename = "dhcp-discover.nse", categories = { "default", "discovery", "intrusive", } }